From c1a63edd71f6337df73a83966a8f283e7f2d8c7b Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 3 Jun 2024 18:28:34 +0200 Subject: [PATCH] Refactor kcContext, avoid having mocks in the dist https://github.com/keycloakify/keycloakify/discussions/299#discussioncomment-9616747 --- src/PUBLIC_URL.ts | 2 +- src/account/index.ts | 4 +- src/account/kcContext/KcContext.ts | 20 +- src/account/kcContext/createGetKcContext.ts | 134 -------- src/account/kcContext/getKcContext.ts | 23 -- .../kcContext/getKcContextFromWindow.ts | 15 - src/account/kcContext/getKcContextMock.ts | 80 +++++ src/account/kcContext/index.ts | 3 +- ..._object_to_js_code_declaring_an_object.ftl | 10 +- src/login/index.ts | 9 +- src/login/kcContext/KcContext.ts | 68 ++-- src/login/kcContext/createGetKcContext.ts | 199 ------------ src/login/kcContext/getKcContext.ts | 23 -- src/login/kcContext/getKcContextFromWindow.ts | 15 - src/login/kcContext/getKcContextMock.ts | 80 +++++ src/login/kcContext/index.ts | 8 +- src/login/kcContext/kcContextMocks.ts | 134 ++++---- src/login/lib/useUserProfileForm.tsx | 8 +- src/tools/AndByDiscriminatingKey.ts | 31 -- src/tools/ExtractAfterStartingWith.ts | 4 + src/tools/ValueOf.ts | 2 + src/tools/deepAssign.ts | 69 ++-- stories/account/createPageStory.tsx | 24 +- stories/account/kcContext.ts | 11 +- stories/account/kcContextMock.ts | 13 + stories/login/kcContext.ts | 2 +- stories/login/kcContextMock.ts | 5 +- test/bin/replacers.spec.ts | 8 +- test/lib/createGetKcContext.spec.ts | 303 ------------------ test/lib/getKcContext.spec.ts | 253 --------------- test/lib/tools/AndByDiscriminatingKey.ts | 100 ------ test/login/kcContext.typelevel-spec.ts | 211 ++++++++++++ test/login/kcContextMock.spec.ts | 202 ++++++++++++ test/tools/isSameCode.ts | 6 - 34 files changed, 834 insertions(+), 1245 deletions(-) delete mode 100644 src/account/kcContext/createGetKcContext.ts delete mode 100644 src/account/kcContext/getKcContext.ts delete mode 100644 src/account/kcContext/getKcContextFromWindow.ts create mode 100644 src/account/kcContext/getKcContextMock.ts delete mode 100644 src/login/kcContext/createGetKcContext.ts delete mode 100644 src/login/kcContext/getKcContext.ts delete mode 100644 src/login/kcContext/getKcContextFromWindow.ts create mode 100644 src/login/kcContext/getKcContextMock.ts delete mode 100644 src/tools/AndByDiscriminatingKey.ts create mode 100644 src/tools/ExtractAfterStartingWith.ts create mode 100644 src/tools/ValueOf.ts create mode 100644 stories/account/kcContextMock.ts delete mode 100644 test/lib/createGetKcContext.spec.ts delete mode 100644 test/lib/getKcContext.spec.ts delete mode 100644 test/lib/tools/AndByDiscriminatingKey.ts create mode 100644 test/login/kcContext.typelevel-spec.ts create mode 100644 test/login/kcContextMock.spec.ts delete mode 100644 test/tools/isSameCode.ts diff --git a/src/PUBLIC_URL.ts b/src/PUBLIC_URL.ts index 050fb499..c21f1f05 100644 --- a/src/PUBLIC_URL.ts +++ b/src/PUBLIC_URL.ts @@ -5,7 +5,7 @@ import { import { assert } from "tsafe/assert"; /** - * This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects. + * This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects. * This works both in your main app and in your Keycloak theme. */ export const PUBLIC_URL = (() => { diff --git a/src/account/index.ts b/src/account/index.ts index ddcab7f7..3bb53c64 100644 --- a/src/account/index.ts +++ b/src/account/index.ts @@ -2,9 +2,9 @@ import Fallback from "keycloakify/account/Fallback"; export default Fallback; -export { getKcContext } from "keycloakify/account/kcContext/getKcContext"; -export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext"; export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants"; export { createUseI18n } from "keycloakify/account/i18n/i18n"; +export type { ExtendKcContext } from "keycloakify/account/kcContext"; +export { createGetKcContextMock } from "keycloakify/account/kcContext"; export type { PageProps } from "keycloakify/account/pages/PageProps"; diff --git a/src/account/kcContext/KcContext.ts b/src/account/kcContext/KcContext.ts index 09242a17..2ab09cff 100644 --- a/src/account/kcContext/KcContext.ts +++ b/src/account/kcContext/KcContext.ts @@ -1,6 +1,24 @@ +import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants"; +import type { ValueOf } from "keycloakify/tools/ValueOf"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; -import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants"; + +export type ExtendKcContext< + KcContextExtraProperties extends { properties?: Record }, + KcContextExtraPropertiesPerPage extends Record> +> = ValueOf<{ + [PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract< + KcContext, + { pageId: PageId } + > extends never + ? KcContext.Common & + KcContextExtraProperties & { + pageId: PageId; + } & KcContextExtraPropertiesPerPage[PageId] + : Extract & + KcContextExtraProperties & + KcContextExtraPropertiesPerPage[PageId]; +}>; export type KcContext = | KcContext.Password diff --git a/src/account/kcContext/createGetKcContext.ts b/src/account/kcContext/createGetKcContext.ts deleted file mode 100644 index f93feab0..00000000 --- a/src/account/kcContext/createGetKcContext.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { DeepPartial } from "keycloakify/tools/DeepPartial"; -import { deepAssign } from "keycloakify/tools/deepAssign"; -import { isStorybook } from "keycloakify/lib/isStorybook"; -import type { ExtendKcContext } from "./getKcContextFromWindow"; -import { getKcContextFromWindow } from "./getKcContextFromWindow"; -import { symToStr } from "tsafe/symToStr"; -import { - kcContextMocks, - kcContextCommonMock -} from "keycloakify/account/kcContext/kcContextMocks"; - -export function createGetKcContext< - KcContextExtension extends { pageId: string } = never ->(params?: { - mockData?: readonly DeepPartial>[]; - mockProperties?: Record; -}) { - const { mockData, mockProperties } = params ?? {}; - - function getKcContext< - PageId extends - | ExtendKcContext["pageId"] - | undefined = undefined - >(params?: { - mockPageId?: PageId; - storyPartialKcContext?: DeepPartial< - Extract, { pageId: PageId }> - >; - }): { - kcContext: PageId extends undefined - ? ExtendKcContext | undefined - : Extract, { pageId: PageId }>; - } { - const { mockPageId, storyPartialKcContext } = params ?? {}; - - const realKcContext = getKcContextFromWindow(); - - if (mockPageId !== undefined && realKcContext === undefined) { - //TODO maybe trow if no mock fo custom page - - warn_that_mock_is_enbaled: { - if (isStorybook) { - break warn_that_mock_is_enbaled; - } - - console.log( - `%cKeycloakify: ${symToStr({ - mockPageId - })} set to ${mockPageId}.`, - "background: red; color: yellow; font-size: medium" - ); - } - - const kcContextDefaultMock = kcContextMocks.find( - ({ pageId }) => pageId === mockPageId - ); - - const partialKcContextCustomMock = (() => { - const out: DeepPartial> = {}; - - const mockDataPick = mockData?.find( - ({ pageId }) => pageId === mockPageId - ); - - if (mockDataPick !== undefined) { - deepAssign({ - target: out, - source: mockDataPick - }); - } - - if (storyPartialKcContext !== undefined) { - deepAssign({ - target: out, - source: storyPartialKcContext - }); - } - - return Object.keys(out).length === 0 ? undefined : out; - })(); - - if ( - kcContextDefaultMock === undefined && - partialKcContextCustomMock === undefined - ) { - console.warn( - [ - `WARNING: You declared the non build in page ${mockPageId} but you didn't `, - `provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`, - `Please check the documentation of the getKcContext function` - ].join("\n") - ); - } - - const kcContext: any = {}; - - deepAssign({ - target: kcContext, - source: - kcContextDefaultMock !== undefined - ? kcContextDefaultMock - : { pageId: mockPageId, ...kcContextCommonMock } - }); - - if (partialKcContextCustomMock !== undefined) { - deepAssign({ - target: kcContext, - source: partialKcContextCustomMock - }); - } - - if (mockProperties !== undefined) { - deepAssign({ - target: kcContext.properties, - source: mockProperties - }); - } - - return { kcContext }; - } - - if (realKcContext === undefined) { - return { kcContext: undefined as any }; - } - - if (realKcContext.themeType !== "account") { - return { kcContext: undefined as any }; - } - - return { kcContext: realKcContext as any }; - } - - return { getKcContext }; -} diff --git a/src/account/kcContext/getKcContext.ts b/src/account/kcContext/getKcContext.ts deleted file mode 100644 index bd6039e8..00000000 --- a/src/account/kcContext/getKcContext.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { DeepPartial } from "keycloakify/tools/DeepPartial"; -import type { ExtendKcContext } from "./getKcContextFromWindow"; -import { createGetKcContext } from "./createGetKcContext"; - -/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier - * See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts - */ -export function getKcContext< - KcContextExtension extends { pageId: string } = never ->(params?: { - mockPageId?: ExtendKcContext["pageId"]; - mockData?: readonly DeepPartial>[]; -}): { kcContext: ExtendKcContext | undefined } { - const { mockPageId, mockData } = params ?? {}; - - const { getKcContext } = createGetKcContext({ - mockData - }); - - const { kcContext } = getKcContext({ mockPageId }); - - return { kcContext }; -} diff --git a/src/account/kcContext/getKcContextFromWindow.ts b/src/account/kcContext/getKcContextFromWindow.ts deleted file mode 100644 index f29ea9bf..00000000 --- a/src/account/kcContext/getKcContextFromWindow.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey"; -import { nameOfTheGlobal } from "keycloakify/bin/shared/constants"; -import type { KcContext } from "./KcContext"; - -export type ExtendKcContext = [ - KcContextExtension -] extends [never] - ? KcContext - : AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>; - -export function getKcContextFromWindow< - KcContextExtension extends { pageId: string } = never ->(): ExtendKcContext | undefined { - return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal]; -} diff --git a/src/account/kcContext/getKcContextMock.ts b/src/account/kcContext/getKcContextMock.ts new file mode 100644 index 00000000..23b32d03 --- /dev/null +++ b/src/account/kcContext/getKcContextMock.ts @@ -0,0 +1,80 @@ +import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext"; +import type { LoginThemePageId } from "keycloakify/bin/shared/constants"; +import type { DeepPartial } from "keycloakify/tools/DeepPartial"; +import { deepAssign } from "keycloakify/tools/deepAssign"; +import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions"; +import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks"; +import { exclude } from "tsafe/exclude"; + +export function createGetKcContextMock< + KcContextExtraProperties extends { properties?: Record }, + KcContextExtraPropertiesPerPage extends Record< + `${string}.ftl`, + Record + > +>(params: { + kcContextExtraProperties: KcContextExtraProperties; + kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage; + overrides?: DeepPartial; + overridesPerPage?: { + [PageId in + | LoginThemePageId + | keyof KcContextExtraPropertiesPerPage]?: DeepPartial< + Extract< + ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >, + { pageId: PageId } + > + >; + }; +}) { + const { + kcContextExtraProperties, + kcContextExtraPropertiesPerPage, + overrides: overrides_global, + overridesPerPage: overridesPerPage_global + } = params; + + type KcContext = ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >; + + function getKcContextMock< + PageId extends LoginThemePageId | keyof KcContextExtraPropertiesPerPage + >(params: { + pageId: PageId; + overrides?: DeepPartial>; + }): Extract { + const { pageId, overrides } = params; + + const kcContextMock = structuredCloneButFunctions( + kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? { + ...kcContextCommonMock, + pageId + } + ); + + [ + kcContextExtraProperties, + kcContextExtraPropertiesPerPage[pageId], + overrides_global, + overridesPerPage_global?.[pageId], + overrides + ] + .filter(exclude(undefined)) + .forEach(overrides => + deepAssign({ + target: kcContextMock, + source: overrides + }) + ); + + // @ts-expect-error + return kcContextMock; + } + + return { getKcContextMock }; +} diff --git a/src/account/kcContext/index.ts b/src/account/kcContext/index.ts index 7ecbb2f3..5990af8d 100644 --- a/src/account/kcContext/index.ts +++ b/src/account/kcContext/index.ts @@ -1 +1,2 @@ -export type { KcContext } from "./KcContext"; +export type { ExtendKcContext, KcContext } from "./KcContext"; +export { createGetKcContextMock } from "./getKcContextMock"; diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl index 8ddf8afe..ef4a1276 100644 --- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl +++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl @@ -288,13 +288,11 @@ function decodeHtmlEntities(htmlStr){ are_same_path(path, []) ) || ( <#-- attributesByName adds a lot of noise to the output and is not needed --> - key == "attributesByName" && - ( - are_same_path(path, ["profile"]) || - are_same_path(path, ["register"]) - ) - ) || ( key == "attributes" && + are_same_path(path, ["profile"]) + ) || ( + <#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object --> + (key == "attributes" || key == "attributesByName") && are_same_path(path, ["register"]) ) > diff --git a/src/login/index.ts b/src/login/index.ts index a0610265..356132ea 100644 --- a/src/login/index.ts +++ b/src/login/index.ts @@ -3,9 +3,12 @@ import Fallback from "keycloakify/login/Fallback"; export default Fallback; export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms"; -export { getKcContext } from "keycloakify/login/kcContext/getKcContext"; -export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext"; -export type { LoginThemePageId as PageId } from "keycloakify/bin/shared/constants"; export { createUseI18n } from "keycloakify/login/i18n/i18n"; +export type { + ExtendKcContext, + Attribute, + PasswordPolicies +} from "keycloakify/login/kcContext"; +export { createGetKcContextMock } from "keycloakify/login/kcContext"; export type { PageProps } from "keycloakify/login/pages/PageProps"; diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts index 834faf1b..c633b271 100644 --- a/src/login/kcContext/KcContext.ts +++ b/src/login/kcContext/KcContext.ts @@ -3,14 +3,28 @@ import type { LoginThemePageId, nameOfTheLocalizationRealmOverridesUserProfileProperty } from "keycloakify/bin/shared/constants"; +import type { ExtractAfterStartingWith } from "keycloakify/tools/ExtractAfterStartingWith"; +import type { ValueOf } from "keycloakify/tools/ValueOf"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import type { MessageKey } from "../i18n/i18n"; -type ExtractAfterStartingWith< - Prefix extends string, - StrEnum -> = StrEnum extends `${Prefix}${infer U}` ? U : never; +export type ExtendKcContext< + KcContextExtraProperties extends { properties?: Record }, + KcContextExtraPropertiesPerPage extends Record> +> = ValueOf<{ + [PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract< + KcContext, + { pageId: PageId } + > extends never + ? KcContext.Common & + KcContextExtraProperties & { + pageId: PageId; + } & KcContextExtraPropertiesPerPage[PageId] + : Extract & + KcContextExtraProperties & + KcContextExtraPropertiesPerPage[PageId]; +}>; /** Take theses type definition with a grain of salt. * Some values might be undefined on some pages. @@ -138,12 +152,12 @@ export declare namespace KcContext { getFirstError: (...fieldNames: string[]) => string; }; - properties: Record; authenticationSession?: { authSessionId: string; tabId: string; ssoLoginInOtherTabsUrl: string; }; + properties: {}; __localizationRealmOverridesUserProfile?: Record; }; @@ -585,7 +599,7 @@ export declare namespace KcContext { } export type UserProfile = { - attributes: Attribute[]; + attributesByName: Record; html5DataAnnotations?: Record; }; @@ -683,31 +697,31 @@ export type Attribute = { | "photo"; }; -export type Validators = Partial<{ - length: Validators.DoIgnoreEmpty & Validators.Range; - integer: Validators.DoIgnoreEmpty & Validators.Range; - email: Validators.DoIgnoreEmpty; - pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string }; - options: Validators.Options; - multivalued: Validators.DoIgnoreEmpty & Validators.Range; +export type Validators = { + length?: Validators.DoIgnoreEmpty & Validators.Range; + integer?: Validators.DoIgnoreEmpty & Validators.Range; + email?: Validators.DoIgnoreEmpty; + pattern?: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string }; + options?: Validators.Options; + multivalued?: Validators.DoIgnoreEmpty & Validators.Range; // NOTE: Following are the validators for which we don't implement client side validation yet // or for which the validation can't be performed on the client side. /* - double: Validators.DoIgnoreEmpty & Validators.Range; - "up-immutable-attribute": {}; - "up-attribute-required-by-metadata-value": {}; - "up-username-has-value": {}; - "up-duplicate-username": {}; - "up-username-mutation": {}; - "up-email-exists-as-username": {}; - "up-blank-attribute-value": Validators.ErrorMessage & { "fail-on-null": boolean; }; - "up-duplicate-email": {}; - "local-date": Validators.DoIgnoreEmpty; - "person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; - uri: Validators.DoIgnoreEmpty; - "username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; + double?: Validators.DoIgnoreEmpty & Validators.Range; + "up-immutable-attribute"?: {}; + "up-attribute-required-by-metadata-value"?: {}; + "up-username-has-value"?: {}; + "up-duplicate-username"?: {}; + "up-username-mutation"?: {}; + "up-email-exists-as-username"?: {}; + "up-blank-attribute-value"?: Validators.ErrorMessage & { "fail-on-null": boolean; }; + "up-duplicate-email"?: {}; + "local-date"?: Validators.DoIgnoreEmpty; + "person-name-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage; + uri?: Validators.DoIgnoreEmpty; + "username-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage; */ -}>; +}; export declare namespace Validators { export type DoIgnoreEmpty = { diff --git a/src/login/kcContext/createGetKcContext.ts b/src/login/kcContext/createGetKcContext.ts deleted file mode 100644 index b051d652..00000000 --- a/src/login/kcContext/createGetKcContext.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { KcContext, Attribute } from "./KcContext"; -import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks"; -import type { DeepPartial } from "keycloakify/tools/DeepPartial"; -import { deepAssign } from "keycloakify/tools/deepAssign"; -import { isStorybook } from "keycloakify/lib/isStorybook"; -import { id } from "tsafe/id"; -import { exclude } from "tsafe/exclude"; -import { assert } from "tsafe/assert"; -import type { ExtendKcContext } from "./getKcContextFromWindow"; -import { getKcContextFromWindow } from "./getKcContextFromWindow"; -import { symToStr } from "tsafe/symToStr"; - -export function createGetKcContext< - KcContextExtension extends { pageId: string } = never ->(params?: { - mockData?: readonly DeepPartial>[]; - mockProperties?: Record; -}) { - const { mockData, mockProperties } = params ?? {}; - - function getKcContext< - PageId extends - | ExtendKcContext["pageId"] - | undefined = undefined - >(params?: { - mockPageId?: PageId; - storyPartialKcContext?: DeepPartial< - Extract, { pageId: PageId }> - >; - }): { - kcContext: PageId extends undefined - ? ExtendKcContext | undefined - : Extract, { pageId: PageId }>; - } { - const { mockPageId, storyPartialKcContext } = params ?? {}; - - const realKcContext = getKcContextFromWindow(); - - if (mockPageId !== undefined && realKcContext === undefined) { - //TODO maybe trow if no mock fo custom page - - warn_that_mock_is_enabled: { - if (isStorybook) { - break warn_that_mock_is_enabled; - } - - console.log( - `%cKeycloakify: ${symToStr({ - mockPageId - })} set to ${mockPageId}.`, - "background: red; color: yellow; font-size: medium" - ); - } - - const kcContextDefaultMock = kcContextMocks.find( - ({ pageId }) => pageId === mockPageId - ); - - const partialKcContextCustomMock = (() => { - const out: DeepPartial> = {}; - - const mockDataPick = mockData?.find( - ({ pageId }) => pageId === mockPageId - ); - - if (mockDataPick !== undefined) { - deepAssign({ - target: out, - source: mockDataPick - }); - } - - if (storyPartialKcContext !== undefined) { - deepAssign({ - target: out, - source: storyPartialKcContext - }); - } - - return Object.keys(out).length === 0 ? undefined : out; - })(); - - if ( - kcContextDefaultMock === undefined && - partialKcContextCustomMock === undefined - ) { - console.warn( - [ - `WARNING: You declared the non build in page ${mockPageId} but you didn't `, - `provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`, - `Please check the documentation of the getKcContext function` - ].join("\n") - ); - } - - const kcContext: any = {}; - - deepAssign({ - target: kcContext, - source: - kcContextDefaultMock !== undefined - ? kcContextDefaultMock - : { pageId: mockPageId, ...kcContextCommonMock } - }); - - if (partialKcContextCustomMock !== undefined) { - deepAssign({ - target: kcContext, - source: partialKcContextCustomMock - }); - - if ("profile" in partialKcContextCustomMock) { - assert( - kcContextDefaultMock !== undefined && - "profile" in kcContextDefaultMock - ); - - const { attributes } = kcContextDefaultMock.profile; - - id(kcContext).profile.attributes = []; - - const partialAttributes = [ - ...(( - partialKcContextCustomMock as DeepPartial - ).profile?.attributes ?? []) - ].filter(exclude(undefined)); - - attributes.forEach(attribute => { - const partialAttribute = partialAttributes.find( - ({ name }) => name === attribute.name - ); - - const augmentedAttribute: Attribute = {} as any; - - deepAssign({ - target: augmentedAttribute, - source: attribute - }); - - if (partialAttribute !== undefined) { - partialAttributes.splice( - partialAttributes.indexOf(partialAttribute), - 1 - ); - - deepAssign({ - target: augmentedAttribute, - source: partialAttribute - }); - } - - id(kcContext).profile.attributes.push( - augmentedAttribute - ); - }); - - partialAttributes - .map(partialAttribute => ({ - validators: {}, - ...partialAttribute - })) - .forEach(partialAttribute => { - const { name } = partialAttribute; - - assert( - name !== undefined, - "If you define a mock attribute it must have at least a name" - ); - - id(kcContext).profile.attributes.push( - partialAttribute as any - ); - }); - } - } - - if (mockProperties !== undefined) { - deepAssign({ - target: kcContext.properties, - source: mockProperties - }); - } - - return { kcContext }; - } - - if (realKcContext === undefined) { - return { kcContext: undefined as any }; - } - - if (realKcContext.themeType !== "login") { - return { kcContext: undefined as any }; - } - - return { kcContext: realKcContext as any }; - } - - return { getKcContext }; -} diff --git a/src/login/kcContext/getKcContext.ts b/src/login/kcContext/getKcContext.ts deleted file mode 100644 index 1e062586..00000000 --- a/src/login/kcContext/getKcContext.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { DeepPartial } from "keycloakify/tools/DeepPartial"; -import type { ExtendKcContext } from "./getKcContextFromWindow"; -import { createGetKcContext } from "./createGetKcContext"; - -/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier - * See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts - */ -export function getKcContext< - KcContextExtension extends { pageId: string } = never ->(params?: { - mockPageId?: ExtendKcContext["pageId"]; - mockData?: readonly DeepPartial>[]; -}): { kcContext: ExtendKcContext | undefined } { - const { mockPageId, mockData } = params ?? {}; - - const { getKcContext } = createGetKcContext({ - mockData - }); - - const { kcContext } = getKcContext({ mockPageId }); - - return { kcContext }; -} diff --git a/src/login/kcContext/getKcContextFromWindow.ts b/src/login/kcContext/getKcContextFromWindow.ts deleted file mode 100644 index ab5f31c4..00000000 --- a/src/login/kcContext/getKcContextFromWindow.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { KcContext } from "./KcContext"; -import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey"; -import { nameOfTheGlobal } from "keycloakify/bin/shared/constants"; - -export type ExtendKcContext = [ - KcContextExtension -] extends [never] - ? KcContext - : AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>; - -export function getKcContextFromWindow< - KcContextExtension extends { pageId: string } = never ->(): ExtendKcContext | undefined { - return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal]; -} diff --git a/src/login/kcContext/getKcContextMock.ts b/src/login/kcContext/getKcContextMock.ts new file mode 100644 index 00000000..23b32d03 --- /dev/null +++ b/src/login/kcContext/getKcContextMock.ts @@ -0,0 +1,80 @@ +import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext"; +import type { LoginThemePageId } from "keycloakify/bin/shared/constants"; +import type { DeepPartial } from "keycloakify/tools/DeepPartial"; +import { deepAssign } from "keycloakify/tools/deepAssign"; +import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions"; +import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks"; +import { exclude } from "tsafe/exclude"; + +export function createGetKcContextMock< + KcContextExtraProperties extends { properties?: Record }, + KcContextExtraPropertiesPerPage extends Record< + `${string}.ftl`, + Record + > +>(params: { + kcContextExtraProperties: KcContextExtraProperties; + kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage; + overrides?: DeepPartial; + overridesPerPage?: { + [PageId in + | LoginThemePageId + | keyof KcContextExtraPropertiesPerPage]?: DeepPartial< + Extract< + ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >, + { pageId: PageId } + > + >; + }; +}) { + const { + kcContextExtraProperties, + kcContextExtraPropertiesPerPage, + overrides: overrides_global, + overridesPerPage: overridesPerPage_global + } = params; + + type KcContext = ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >; + + function getKcContextMock< + PageId extends LoginThemePageId | keyof KcContextExtraPropertiesPerPage + >(params: { + pageId: PageId; + overrides?: DeepPartial>; + }): Extract { + const { pageId, overrides } = params; + + const kcContextMock = structuredCloneButFunctions( + kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? { + ...kcContextCommonMock, + pageId + } + ); + + [ + kcContextExtraProperties, + kcContextExtraPropertiesPerPage[pageId], + overrides_global, + overridesPerPage_global?.[pageId], + overrides + ] + .filter(exclude(undefined)) + .forEach(overrides => + deepAssign({ + target: kcContextMock, + source: overrides + }) + ); + + // @ts-expect-error + return kcContextMock; + } + + return { getKcContextMock }; +} diff --git a/src/login/kcContext/index.ts b/src/login/kcContext/index.ts index 7ecbb2f3..18737f02 100644 --- a/src/login/kcContext/index.ts +++ b/src/login/kcContext/index.ts @@ -1 +1,7 @@ -export type { KcContext } from "./KcContext"; +export type { + ExtendKcContext, + KcContext, + Attribute, + PasswordPolicies +} from "./KcContext"; +export { createGetKcContextMock } from "./getKcContextMock"; diff --git a/src/login/kcContext/kcContextMocks.ts b/src/login/kcContext/kcContextMocks.ts index 9d81ecc9..7d74c96a 100644 --- a/src/login/kcContext/kcContextMocks.ts +++ b/src/login/kcContext/kcContextMocks.ts @@ -9,71 +9,73 @@ import { id } from "tsafe/id"; import { assert, type Equals } from "tsafe/assert"; import { BASE_URL } from "keycloakify/lib/BASE_URL"; -const attributes: Attribute[] = [ - { - validators: { - length: { - "ignore.empty.value": true, - min: "3", - max: "255" - } - }, - displayName: "${username}", - annotations: {}, - required: true, - autocomplete: "username", - readOnly: false, - name: "username", - value: "xxxx" - }, - { - validators: { - length: { - max: "255", - "ignore.empty.value": true +const attributesByName = Object.fromEntries( + id([ + { + validators: { + length: { + "ignore.empty.value": true, + min: "3", + max: "255" + } }, - email: { - "ignore.empty.value": true + displayName: "${username}", + annotations: {}, + required: true, + autocomplete: "username", + readOnly: false, + name: "username", + value: "xxxx" + }, + { + validators: { + length: { + max: "255", + "ignore.empty.value": true + }, + email: { + "ignore.empty.value": true + }, + pattern: { + "ignore.empty.value": true, + pattern: "gmail\\.com$" + } }, - pattern: { - "ignore.empty.value": true, - pattern: "gmail\\.com$" - } + displayName: "${email}", + annotations: {}, + required: true, + autocomplete: "email", + readOnly: false, + name: "email" }, - displayName: "${email}", - annotations: {}, - required: true, - autocomplete: "email", - readOnly: false, - name: "email" - }, - { - validators: { - length: { - max: "255", - "ignore.empty.value": true - } + { + validators: { + length: { + max: "255", + "ignore.empty.value": true + } + }, + displayName: "${firstName}", + annotations: {}, + required: true, + readOnly: false, + name: "firstName" }, - displayName: "${firstName}", - annotations: {}, - required: true, - readOnly: false, - name: "firstName" - }, - { - validators: { - length: { - max: "255", - "ignore.empty.value": true - } - }, - displayName: "${lastName}", - annotations: {}, - required: true, - readOnly: false, - name: "lastName" - } -]; + { + validators: { + length: { + max: "255", + "ignore.empty.value": true + } + }, + displayName: "${lastName}", + annotations: {}, + required: true, + readOnly: false, + name: "lastName" + } + ]).map(attribute => [attribute.name, attribute]) +); const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`; @@ -265,7 +267,7 @@ export const kcContextMocks = [ recaptchaRequired: false, pageId: "register.ftl", profile: { - attributes + attributesByName }, scripts: [ //"https://www.google.com/recaptcha/api.js" @@ -416,7 +418,7 @@ export const kcContextMocks = [ ...kcContextCommonMock, pageId: "login-update-profile.ftl", profile: { - attributes + attributesByName } }), id({ @@ -472,14 +474,16 @@ export const kcContextMocks = [ ...kcContextCommonMock, pageId: "idp-review-user-profile.ftl", profile: { - attributes + attributesByName } }), id({ ...kcContextCommonMock, pageId: "update-email.ftl", profile: { - attributes: attributes.filter(attribute => attribute.name === "email") + attributesByName: { + email: attributesByName["email"] + } } }), id({ diff --git a/src/login/lib/useUserProfileForm.tsx b/src/login/lib/useUserProfileForm.tsx index c265a470..309d7a93 100644 --- a/src/login/lib/useUserProfileForm.tsx +++ b/src/login/lib/useUserProfileForm.tsx @@ -9,7 +9,7 @@ import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/Kc import { assert, type Equals } from "tsafe/assert"; import { formatNumber } from "keycloakify/tools/formatNumber"; import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; -import { structuredCloneButFunctions } from "tools/structuredCloneButFunctions"; +import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions"; import type { I18n } from "../i18n"; export type FormFieldError = { @@ -68,7 +68,7 @@ export type FormAction = export type KcContextLike = { messagesPerField: Pick; profile: { - attributes: Attribute[]; + attributesByName: Record; html5DataAnnotations?: Record; }; passwordRequired?: boolean; @@ -137,7 +137,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy const attributes = (() => { retrocompat_patch: { - if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) { + if ("profile" in kcContext && "attributes" in kcContext.profile && Object.keys(kcContext.profile.attributesByName).length !== 0) { break retrocompat_patch; } @@ -217,7 +217,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy assert(false, "Unable to mock user profile from the current kcContext"); } - return kcContext.profile.attributes.map(attribute_pre_group_patch => { + return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => { if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") { const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = attribute_pre_group_patch as Attribute & { diff --git a/src/tools/AndByDiscriminatingKey.ts b/src/tools/AndByDiscriminatingKey.ts deleted file mode 100644 index 79563580..00000000 --- a/src/tools/AndByDiscriminatingKey.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type AndByDiscriminatingKey< - DiscriminatingKey extends string, - U1 extends Record, - U2 extends Record -> = AndByDiscriminatingKey.Tf1; - -export declare namespace AndByDiscriminatingKey { - export type Tf1< - DiscriminatingKey extends string, - U1, - U1Again extends Record, - U2 extends Record - > = - U1 extends Pick - ? Tf2 - : U1Again[DiscriminatingKey] & U2[DiscriminatingKey] extends never - ? U1 | U2 - : U1; - - export type Tf2< - DiscriminatingKey extends string, - SingletonU1 extends Record, - U2, - U1 extends Record - > = - U2 extends Pick - ? U2 & SingletonU1 - : U2 extends Pick - ? never - : U2; -} diff --git a/src/tools/ExtractAfterStartingWith.ts b/src/tools/ExtractAfterStartingWith.ts new file mode 100644 index 00000000..9674b22e --- /dev/null +++ b/src/tools/ExtractAfterStartingWith.ts @@ -0,0 +1,4 @@ +export type ExtractAfterStartingWith< + Prefix extends string, + StrEnum +> = StrEnum extends `${Prefix}${infer U}` ? U : never; diff --git a/src/tools/ValueOf.ts b/src/tools/ValueOf.ts new file mode 100644 index 00000000..031db534 --- /dev/null +++ b/src/tools/ValueOf.ts @@ -0,0 +1,2 @@ +/** Pendant of `keyof T` */ +export type ValueOf = T[keyof T]; diff --git a/src/tools/deepAssign.ts b/src/tools/deepAssign.ts index 6c7c693c..04fa75b9 100644 --- a/src/tools/deepAssign.ts +++ b/src/tools/deepAssign.ts @@ -2,44 +2,60 @@ import { assert } from "tsafe/assert"; import { is } from "tsafe/is"; import { structuredCloneButFunctions } from "./structuredCloneButFunctions"; -//Warning: Be mindful that because of array this is not idempotent. +/** NOTE: Array a copied over, not merged. */ export function deepAssign(params: { target: Record; source: Record; -}) { - const { target } = params; - - const source = structuredCloneButFunctions(params.source); +}): void { + const { target, source } = params; Object.keys(source).forEach(key => { var dereferencedSource = source[key]; + if (dereferencedSource === undefined) { + delete target[key]; + return; + } + + if (dereferencedSource instanceof Date) { + assign({ + target, + key, + value: new Date(dereferencedSource.getTime()) + }); + + return; + } + + if (dereferencedSource instanceof Array) { + assign({ + target, + key, + value: structuredCloneButFunctions(dereferencedSource) + }); + + return; + } + if ( - target[key] === undefined || dereferencedSource instanceof Function || !(dereferencedSource instanceof Object) ) { - Object.defineProperty(target, key, { - enumerable: true, - writable: true, - configurable: true, + assign({ + target, + key, value: dereferencedSource }); return; } - const dereferencedTarget = target[key]; - - if (dereferencedSource instanceof Array) { - assert(is(dereferencedTarget)); - assert(is(dereferencedSource)); - - dereferencedSource.forEach(entry => dereferencedTarget.push(entry)); - - return; + if (!(target[key] instanceof Object)) { + target[key] = {}; } + const dereferencedTarget = target[key]; + assert(is>(dereferencedTarget)); assert(is>(dereferencedSource)); @@ -49,3 +65,18 @@ export function deepAssign(params: { }); }); } + +function assign(params: { + target: Record; + key: string; + value: unknown; +}): void { + const { target, key, value } = params; + + Object.defineProperty(target, key, { + enumerable: true, + writable: true, + configurable: true, + value + }); +} diff --git a/stories/account/createPageStory.tsx b/stories/account/createPageStory.tsx index d768e803..add3e5f6 100644 --- a/stories/account/createPageStory.tsx +++ b/stories/account/createPageStory.tsx @@ -1,19 +1,31 @@ import React from "react"; -import { getKcContext, type KcContext } from "./kcContext"; +import type { KcContext } from "./kcContext"; +import { getKcContextMock } from "./kcContextMock"; import KcApp from "./KcApp"; import type { DeepPartial } from "../../dist/tools/DeepPartial"; export function createPageStory(params: { pageId: PageId }) { const { pageId } = params; - function PageStory(params: { kcContext?: DeepPartial> }) { - const { kcContext } = getKcContext({ - mockPageId: pageId, - storyPartialKcContext: params.kcContext + function PageStory(props: { kcContext?: DeepPartial> }) { + const { kcContext: overrides } = props; + + const kcContextMock = getKcContextMock({ + pageId, + overrides }); - return ; + return ; } return { PageStory }; } + +export const parameters = { + viewMode: "story", + previewTabs: { + "storybook/docs/panel": { + hidden: true + } + } +}; diff --git a/stories/account/kcContext.ts b/stories/account/kcContext.ts index 392dcd08..e8144305 100644 --- a/stories/account/kcContext.ts +++ b/stories/account/kcContext.ts @@ -1,7 +1,10 @@ -import { createGetKcContext } from "../../dist/account"; +import type { ExtendKcContext } from "../../dist/account"; -export const { getKcContext } = createGetKcContext(); +export type KcContextExtraProperties = {}; -const { kcContext } = getKcContext(); +export type KcContextExtraPropertiesPerPage = {}; -export type KcContext = NonNullable; +export type KcContext = ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage +>; diff --git a/stories/account/kcContextMock.ts b/stories/account/kcContextMock.ts new file mode 100644 index 00000000..88069124 --- /dev/null +++ b/stories/account/kcContextMock.ts @@ -0,0 +1,13 @@ +import { createGetKcContextMock } from "../../dist/account"; +import type { + KcContextExtraProperties, + KcContextExtraPropertiesPerPage +} from "./kcContext"; + +const kcContextExtraProperties: KcContextExtraProperties = {}; +const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {}; + +export const { getKcContextMock } = createGetKcContextMock({ + kcContextExtraProperties, + kcContextExtraPropertiesPerPage +}); diff --git a/stories/login/kcContext.ts b/stories/login/kcContext.ts index fca17d6c..03dec7b8 100644 --- a/stories/login/kcContext.ts +++ b/stories/login/kcContext.ts @@ -1,4 +1,4 @@ -import { ExtendKcContext } from "../../dist/login"; +import type { ExtendKcContext } from "../../dist/login"; export type KcContextExtraProperties = {}; diff --git a/stories/login/kcContextMock.ts b/stories/login/kcContextMock.ts index 53efb9c6..d248db14 100644 --- a/stories/login/kcContextMock.ts +++ b/stories/login/kcContextMock.ts @@ -1,5 +1,8 @@ import { createGetKcContextMock } from "../../dist/login"; -import { KcContextExtraProperties, KcContextExtraPropertiesPerPage } from "./kcContext"; +import type { + KcContextExtraProperties, + KcContextExtraPropertiesPerPage +} from "./kcContext"; const kcContextExtraProperties: KcContextExtraProperties = {}; const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {}; diff --git a/test/bin/replacers.spec.ts b/test/bin/replacers.spec.ts index da8d11fd..b33251ea 100644 --- a/test/bin/replacers.spec.ts +++ b/test/bin/replacers.spec.ts @@ -7,7 +7,6 @@ import { import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode"; import { same } from "evt/tools/inDepth/same"; import { expect, it, describe } from "vitest"; -import { isSameCode } from "../tools/isSameCode"; import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal @@ -664,3 +663,10 @@ describe("inline css replacer", () => { }); }); }); + +export function isSameCode(code1: string, code2: string): boolean { + const removeSpacesAndNewLines = (code: string) => + code.replace(/\s/g, "").replace(/\n/g, ""); + + return removeSpacesAndNewLines(code1) === removeSpacesAndNewLines(code2); +} diff --git a/test/lib/createGetKcContext.spec.ts b/test/lib/createGetKcContext.spec.ts deleted file mode 100644 index 368191fd..00000000 --- a/test/lib/createGetKcContext.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext"; -import type { ExtendKcContext } from "keycloakify/login/kcContext/getKcContextFromWindow"; -import type { KcContext } from "keycloakify/login/kcContext"; -import { same } from "evt/tools/inDepth"; -import { assert } from "tsafe/assert"; -import type { Equals } from "tsafe"; -import { - kcContextMocks, - kcContextCommonMock -} from "keycloakify/login/kcContext/kcContextMocks"; -import { deepClone } from "keycloakify/tools/deepClone"; -import { expect, it, describe } from "vitest"; - -describe("createGetKcContext", () => { - const authorizedMailDomains = [ - "example.com", - "another-example.com", - "*.yet-another-example.com", - "*.example.com", - "hello-world.com" - ]; - - const displayName = "this is an overwritten common value"; - - const aNonStandardValue1 = "a non standard value 1"; - const aNonStandardValue2 = "a non standard value 2"; - - type KcContextExtension = - | { - pageId: "register.ftl"; - authorizedMailDomains: string[]; - } - | { - pageId: "info.ftl"; - aNonStandardValue1: string; - } - | { - pageId: "my-extra-page-1.ftl"; - } - | { - pageId: "my-extra-page-2.ftl"; - aNonStandardValue2: string; - }; - - const getKcContextProxy = (params: { - mockPageId: ExtendKcContext["pageId"]; - }) => { - const { mockPageId } = params; - - const { getKcContext } = createGetKcContext({ - mockData: [ - { - pageId: "login.ftl", - realm: { displayName } - }, - { - pageId: "info.ftl", - aNonStandardValue1 - }, - { - pageId: "register.ftl", - authorizedMailDomains - }, - { - pageId: "my-extra-page-2.ftl", - aNonStandardValue2 - } - ] - }); - - const { kcContext } = getKcContext({ - mockPageId - }); - - return { kcContext }; - }; - it("has proper API for login.ftl", () => { - const pageId = "login.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert>(); - - expect( - same( - //NOTE: deepClone for printIfExists or other functions... - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - mock.realm.displayName = displayName; - - return mock; - })() - ) - ).toBe(true); - }); - - it("has a proper API for info.ftl", () => { - const pageId = "info.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - //NOTE: I don't understand the need to add: pageId: typeof pageId; ... - assert< - Equals< - typeof kcContext, - KcContext.Info & { - pageId: typeof pageId; - aNonStandardValue1: string; - } - > - >(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - Object.assign(mock, { aNonStandardValue1 }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for register.ftl", () => { - const pageId = "register.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - //NOTE: I don't understand the need to add: pageId: typeof pageId; ... - assert< - Equals< - typeof kcContext, - KcContext.Register & { - pageId: typeof pageId; - authorizedMailDomains: string[]; - } - > - >(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - Object.assign(mock, { authorizedMailDomains }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for my-extra-page-2.ftl", () => { - const pageId = "my-extra-page-2.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert< - Equals< - typeof kcContext, - KcContext.Common & { - pageId: typeof pageId; - aNonStandardValue2: string; - } - > - >(); - - kcContext.aNonStandardValue2; - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone(kcContextCommonMock); - - Object.assign(mock, { pageId, aNonStandardValue2 }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for my-extra-page-1.ftl", () => { - const pageId = "my-extra-page-1.ftl"; - - console.log("We expect a warning here =>"); - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert>(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone(kcContextCommonMock); - - Object.assign(mock, { pageId }); - - return mock; - })() - ) - ).toBe(true); - }); - it("returns the proper mock for login.ftl", () => { - const pageId = "login.ftl"; - - const { getKcContext } = createGetKcContext(); - - const { kcContext } = getKcContext({ - mockPageId: pageId - }); - - assert>(); - - assert( - same( - deepClone(kcContext), - deepClone( - kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)! - ) - ) - ); - }); - it("returns undefined when no mock is specified", () => { - const { getKcContext } = createGetKcContext(); - - const { kcContext } = getKcContext(); - - assert>(); - - assert(kcContext === undefined); - }); - - it("mock are properly overwritten", () => { - const { getKcContext } = createGetKcContext(); - - const displayName = "myDisplayName"; - - const { kcContext } = getKcContext({ - mockPageId: "login.ftl", - storyPartialKcContext: { - realm: { - displayName - } - } - }); - - assert>(); - - assert(kcContext?.realm.displayName === displayName); - }); - - it("mockPageId doesn't have to be a singleton", () => { - const { getKcContext } = createGetKcContext(); - - const mockPageId: "login.ftl" | "register.ftl" = "login.ftl" as any; - - const { kcContext } = getKcContext({ - mockPageId - }); - - assert>(); - }); - - it("no undefined as long as we provide a mock pageId", () => { - const { getKcContext } = createGetKcContext(); - - const mockPageId: KcContext["pageId"] = "login.ftl" as any; - - const { kcContext } = getKcContext({ - mockPageId - }); - - assert>(); - }); -}); diff --git a/test/lib/getKcContext.spec.ts b/test/lib/getKcContext.spec.ts deleted file mode 100644 index 09f13d57..00000000 --- a/test/lib/getKcContext.spec.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { getKcContext } from "keycloakify/login/kcContext/getKcContext"; -import type { ExtendKcContext } from "keycloakify/login/kcContext/getKcContextFromWindow"; -import type { KcContext } from "keycloakify/login/kcContext"; -import { same } from "evt/tools/inDepth"; -import { assert } from "tsafe/assert"; -import type { Equals } from "tsafe"; -import { - kcContextMocks, - kcContextCommonMock -} from "keycloakify/login/kcContext/kcContextMocks"; -import { deepClone } from "keycloakify/tools/deepClone"; -import { expect, it, describe } from "vitest"; - -describe("getKcContext", () => { - const authorizedMailDomains = [ - "example.com", - "another-example.com", - "*.yet-another-example.com", - "*.example.com", - "hello-world.com" - ]; - - const displayName = "this is an overwritten common value"; - - const aNonStandardValue1 = "a non standard value 1"; - const aNonStandardValue2 = "a non standard value 2"; - - type KcContextExtension = - | { - pageId: "register.ftl"; - authorizedMailDomains: string[]; - } - | { - pageId: "info.ftl"; - aNonStandardValue1: string; - } - | { - pageId: "my-extra-page-1.ftl"; - } - | { - pageId: "my-extra-page-2.ftl"; - aNonStandardValue2: string; - }; - - const getKcContextProxy = (params: { - mockPageId: ExtendKcContext["pageId"]; - }) => { - const { mockPageId } = params; - - const { kcContext } = getKcContext({ - mockPageId, - mockData: [ - { - pageId: "login.ftl", - realm: { displayName } - }, - { - pageId: "info.ftl", - aNonStandardValue1 - }, - { - pageId: "register.ftl", - authorizedMailDomains - }, - { - pageId: "my-extra-page-2.ftl", - aNonStandardValue2 - } - ] - }); - - return { kcContext }; - }; - it("has proper API for login.ftl", () => { - const pageId = "login.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert>(); - - expect( - same( - //NOTE: deepClone for printIfExists or other functions... - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - mock.realm.displayName = displayName; - - return mock; - })() - ) - ).toBe(true); - }); - - it("has a proper API for info.ftl", () => { - const pageId = "info.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - //NOTE: I don't understand the need to add: pageId: typeof pageId; ... - assert< - Equals< - typeof kcContext, - KcContext.Info & { - pageId: typeof pageId; - aNonStandardValue1: string; - } - > - >(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - Object.assign(mock, { aNonStandardValue1 }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for register.ftl", () => { - const pageId = "register.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - //NOTE: I don't understand the need to add: pageId: typeof pageId; ... - assert< - Equals< - typeof kcContext, - KcContext.Register & { - pageId: typeof pageId; - authorizedMailDomains: string[]; - } - > - >(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone( - kcContextMocks.find( - ({ pageId: pageId_i }) => pageId_i === pageId - )! - ); - - Object.assign(mock, { authorizedMailDomains }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for my-extra-page-2.ftl", () => { - const pageId = "my-extra-page-2.ftl"; - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert< - Equals< - typeof kcContext, - KcContext.Common & { - pageId: typeof pageId; - aNonStandardValue2: string; - } - > - >(); - - kcContext.aNonStandardValue2; - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone(kcContextCommonMock); - - Object.assign(mock, { pageId, aNonStandardValue2 }); - - return mock; - })() - ) - ).toBe(true); - }); - it("has a proper API for my-extra-page-1.ftl", () => { - const pageId = "my-extra-page-1.ftl"; - - console.log("We expect a warning here =>"); - - const { kcContext } = getKcContextProxy({ mockPageId: pageId }); - - assert(kcContext?.pageId === pageId); - - assert>(); - - expect( - same( - deepClone(kcContext), - (() => { - const mock = deepClone(kcContextCommonMock); - - Object.assign(mock, { pageId }); - - return mock; - })() - ) - ).toBe(true); - }); - it("returns the proper mock for login.ftl", () => { - const pageId = "login.ftl"; - - const { kcContext } = getKcContext({ - mockPageId: pageId - }); - - assert>(); - - assert( - same( - deepClone(kcContext), - deepClone( - kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)! - ) - ) - ); - }); - it("returns undefined when no mock is specified", () => { - const { kcContext } = getKcContext(); - - assert>(); - - assert(kcContext === undefined); - }); -}); diff --git a/test/lib/tools/AndByDiscriminatingKey.ts b/test/lib/tools/AndByDiscriminatingKey.ts deleted file mode 100644 index 61fee1aa..00000000 --- a/test/lib/tools/AndByDiscriminatingKey.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey"; -import { assert } from "tsafe/assert"; -import type { Equals } from "tsafe"; - -{ - type Base = - | { pageId: "a"; onlyA: string } - | { pageId: "b"; onlyB: string } - | { pageId: "only base"; onlyBase: string }; - - type Extension = - | { pageId: "a"; onlyExtA: string } - | { pageId: "b"; onlyExtB: string } - | { pageId: "only ext"; onlyExt: string }; - - type Got = AndByDiscriminatingKey<"pageId", Extension, Base>; - - type Expected = - | { pageId: "a"; onlyA: string; onlyExtA: string } - | { pageId: "b"; onlyB: string; onlyExtB: string } - | { pageId: "only base"; onlyBase: string } - | { pageId: "only ext"; onlyExt: string }; - - assert>(); - - const x: Got = null as any; - - if (x.pageId === "a") { - x.onlyA; - x.onlyExtA; - - //@ts-expect-error - x.onlyB; - - //@ts-expect-error - x.onlyBase; - - //@ts-expect-error - x.onlyExt; - } - - if (x.pageId === "b") { - x.onlyB; - x.onlyExtB; - - //@ts-expect-error - x.onlyA; - - //@ts-expect-error - x.onlyBase; - - //@ts-expect-error - x.onlyExt; - } - - if (x.pageId === "only base") { - x.onlyBase; - - //@ts-expect-error - x.onlyA; - - //@ts-expect-error - x.onlyB; - - //@ts-expect-error - x.onlyExt; - } - - if (x.pageId === "only ext") { - x.onlyExt; - - //@ts-expect-error - x.onlyA; - - //@ts-expect-error - x.onlyB; - - //@ts-expect-error - x.onlyBase; - } -} - -{ - type Base = - | { pageId: "a"; onlyA: string } - | { pageId: "b"; onlyB: string } - | { pageId: "only base"; onlyBase: string }; - - type Extension = { pageId: "only ext"; onlyExt: string }; - - type Got = AndByDiscriminatingKey<"pageId", Extension, Base>; - - type Expected = - | { pageId: "a"; onlyA: string } - | { pageId: "b"; onlyB: string } - | { pageId: "only base"; onlyBase: string } - | { pageId: "only ext"; onlyExt: string }; - - assert>(); -} diff --git a/test/login/kcContext.typelevel-spec.ts b/test/login/kcContext.typelevel-spec.ts new file mode 100644 index 00000000..0cb2fbb8 --- /dev/null +++ b/test/login/kcContext.typelevel-spec.ts @@ -0,0 +1,211 @@ +import { type ExtendKcContext, createGetKcContextMock } from "keycloakify/login"; +import { KcContext as KcContextBase } from "keycloakify/login/kcContext/KcContext"; +import { assert, type Equals } from "tsafe/assert"; +import { Reflect } from "tsafe/Reflect"; + +{ + type KcContextExtraProperties = { + properties: { + myCustomProperty: string | undefined; + }; + }; + + type KcContextExtraPropertiesPerPage = { + "login.ftl": { + foo: string; + }; + "my-custom-page.ftl": { + bar: number; + }; + }; + + type KcContext = ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >; + + { + type Got = Extract; + type Expected = KcContextBase.Login & { + properties: { myCustomProperty: string | undefined }; + } & { foo: string }; + + assert>(); + } + + { + type Got = Extract; + type Expected = KcContextBase.Register & { + properties: { myCustomProperty: string | undefined }; + }; + + assert>(); + } + + { + type Got = Extract; + + type Expected = KcContextBase.Common & + KcContextExtraProperties & { pageId: "my-custom-page.ftl" } & { + properties: { myCustomProperty: string | undefined }; + } & { bar: number }; + + assert(); + assert(); + } + + const { getKcContextMock } = createGetKcContextMock({ + kcContextExtraProperties: Reflect(), + kcContextExtraPropertiesPerPage: Reflect() + }); + + { + const got = getKcContextMock({ + pageId: "login.ftl" + }); + + type Expected = Extract; + + assert>(); + } + + { + const got = getKcContextMock({ + pageId: "register.ftl" + }); + + type Expected = Extract; + + assert>(); + } + + { + const got = getKcContextMock({ + pageId: "my-custom-page.ftl" + }); + + type Expected = Extract; + + assert>(); + } + + getKcContextMock({ + // @ts-expect-error + pageId: "non-existing-page.ftl" + }); + + getKcContextMock({ + pageId: "login.ftl", + overrides: { + // @ts-expect-error + bar: 42 + } + }); + + createGetKcContextMock({ + kcContextExtraProperties: Reflect(), + kcContextExtraPropertiesPerPage: Reflect(), + overrides: { + locale: { + currentLanguageTag: "fr" + }, + // @ts-expect-error + profile: {} + }, + overridesPerPage: { + "register.ftl": { + profile: { + attributesByName: { + username: { + validators: { + pattern: { + pattern: "^[a-zA-Z0-9]+$", + "ignore.empty.value": true, + "error-message": "${alphanumericalCharsOnly}" + } + }, + value: undefined, + name: "username" + } + } + } + }, + // @ts-expect-error + "non-existing-page.ftl": {} + } + }); + + createGetKcContextMock({ + kcContextExtraProperties: Reflect(), + kcContextExtraPropertiesPerPage: Reflect(), + overridesPerPage: { + "register.ftl": { + // @ts-expect-error + nonExistingProperty: 42 + } + } + }); +} + +{ + type KcContextExtraProperties = {}; + + type KcContextExtraPropertiesPerPage = {}; + + type KcContext = ExtendKcContext< + KcContextExtraProperties, + KcContextExtraPropertiesPerPage + >; + + { + type Got = Extract; + type Expected = KcContextBase.Login; + + assert>(); + } + + { + type Got = Extract; + type Expected = KcContextBase.Register; + + assert>(); + } + + const { getKcContextMock } = createGetKcContextMock({ + kcContextExtraProperties: Reflect(), + kcContextExtraPropertiesPerPage: Reflect() + }); + + { + const got = getKcContextMock({ + pageId: "login.ftl" + }); + + type Expected = Extract; + + assert>(); + } + + { + const got = getKcContextMock({ + pageId: "register.ftl" + }); + + type Expected = Extract; + + assert>(); + } + + getKcContextMock({ + // @ts-expect-error + pageId: "non-existing-page.ftl" + }); + + getKcContextMock({ + pageId: "login.ftl", + overrides: { + // @ts-expect-error + bar: 42 + } + }); +} diff --git a/test/login/kcContextMock.spec.ts b/test/login/kcContextMock.spec.ts new file mode 100644 index 00000000..3e259ffd --- /dev/null +++ b/test/login/kcContextMock.spec.ts @@ -0,0 +1,202 @@ +import { createGetKcContextMock, type Attribute } from "keycloakify/login"; +import { id } from "tsafe/id"; +import { + kcContextMocks, + kcContextCommonMock +} from "keycloakify/login/kcContext/kcContextMocks"; +import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions"; +import { expect, it, describe } from "vitest"; + +describe("createGetKcContextMock", () => { + type KcContextExtraProperties = { + properties: { + MY_ENV_VAR?: string; + }; + }; + + type KcContextExtraPropertiesPerPage = { + "register.ftl": { + authorizedMailDomains: string[]; + }; + "my-plugin-page.ftl": { + aCustomValue: string; + }; + }; + + const { getKcContextMock } = createGetKcContextMock({ + kcContextExtraProperties: id({ + properties: { + MY_ENV_VAR: "my env var value" + } + }), + kcContextExtraPropertiesPerPage: id({ + "register.ftl": { + authorizedMailDomains: ["gmail.com", "hotmail.com"] + }, + "my-plugin-page.ftl": { + aCustomValue: "some value" + } + }), + overrides: { + locale: { + currentLanguageTag: "fr" + } + }, + overridesPerPage: { + "register.ftl": { + profile: { + attributesByName: { + username: { + validators: { + pattern: { + pattern: "^[a-zA-Z0-9]+$", + "ignore.empty.value": true, + "error-message": "${alphanumericalCharsOnly}" + } + }, + value: undefined, + name: "username" + } + } + }, + passwordPolicies: { + length: 66 + } + } + } + }); + + it("returns the proper mock for register.frl", () => { + const got = getKcContextMock({ + pageId: "register.ftl", + overrides: { + profile: { + attributesByName: { + gender: id({ + validators: { + options: { + options: [ + "male", + "female", + "non-binary", + "prefer-not-to-say" + ] + } + }, + displayName: "${gender}", + annotations: {}, + required: true, + readOnly: false, + name: "gender" + }), + email: undefined + } + } + } + }); + + const expected = (() => { + const out: any = structuredCloneButFunctions( + kcContextMocks.find(({ pageId }) => pageId === "register.ftl") + ); + + out.properties = { + MY_ENV_VAR: "my env var value" + }; + + out.authorizedMailDomains = ["gmail.com", "hotmail.com"]; + + out.locale.currentLanguageTag = "fr"; + + delete out.profile.attributesByName.email; + + { + const usernameAttribute = out.profile.attributesByName.username; + + delete usernameAttribute.value; + usernameAttribute.validators.pattern = { + pattern: "^[a-zA-Z0-9]+$", + "ignore.empty.value": true, + "error-message": "${alphanumericalCharsOnly}" + }; + } + + out.profile.attributesByName.gender = { + validators: { + options: { + options: ["male", "female", "non-binary", "prefer-not-to-say"] + } + }, + displayName: "${gender}", + annotations: {}, + required: true, + readOnly: false, + name: "gender" + }; + + (out.passwordPolicies ??= {}).length = 66; + + return out; + })(); + + expect(got).toEqual(expected); + }); + + it("returns the proper mock plugin pages", () => { + const got = getKcContextMock({ + pageId: "my-plugin-page.ftl", + overrides: { + locale: { + currentLanguageTag: "en" + } + } + }); + + const expected = (() => { + const out: any = structuredCloneButFunctions(kcContextCommonMock); + + out.pageId = "my-plugin-page.ftl"; + + out.aCustomValue = "some value"; + + out.properties = { + MY_ENV_VAR: "my env var value" + }; + + out.locale.currentLanguageTag = "en"; + + return out; + })(); + + expect(got).toEqual(expected); + }); + + it("returns the proper mock for other pages", () => { + const got = getKcContextMock({ + pageId: "login.ftl", + overrides: { + realm: { + registrationAllowed: false + } + } + }); + + const expected = (() => { + const out: any = structuredCloneButFunctions( + kcContextMocks.find(({ pageId }) => pageId === "login.ftl") + ); + + out.properties = { + MY_ENV_VAR: "my env var value" + }; + + out.locale.currentLanguageTag = "fr"; + + out.realm.registrationAllowed = false; + + return out; + })(); + + expect(got).toEqual(expected); + }); +}); diff --git a/test/tools/isSameCode.ts b/test/tools/isSameCode.ts deleted file mode 100644 index d9252c12..00000000 --- a/test/tools/isSameCode.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function isSameCode(code1: string, code2: string): boolean { - const removeSpacesAndNewLines = (code: string) => - code.replace(/\s/g, "").replace(/\n/g, ""); - - return removeSpacesAndNewLines(code1) === removeSpacesAndNewLines(code2); -}