This commit is contained in:
Joseph Garrone 2024-06-03 18:28:34 +02:00
parent 37a060c4db
commit c1a63edd71
34 changed files with 834 additions and 1245 deletions

View File

@ -5,7 +5,7 @@ import {
import { assert } from "tsafe/assert"; 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. * This works both in your main app and in your Keycloak theme.
*/ */
export const PUBLIC_URL = (() => { export const PUBLIC_URL = (() => {

View File

@ -2,9 +2,9 @@ import Fallback from "keycloakify/account/Fallback";
export default 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 type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
export { createUseI18n } from "keycloakify/account/i18n/i18n"; 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"; export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -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 { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
export type ExtendKcContext<
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
> = ValueOf<{
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
KcContext,
{ pageId: PageId }
> extends never
? KcContext.Common &
KcContextExtraProperties & {
pageId: PageId;
} & KcContextExtraPropertiesPerPage[PageId]
: Extract<KcContext, { pageId: PageId }> &
KcContextExtraProperties &
KcContextExtraPropertiesPerPage[PageId];
}>;
export type KcContext = export type KcContext =
| KcContext.Password | KcContext.Password

View File

@ -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<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
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<ExtendKcContext<KcContextExtension>> = {};
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 };
}

View File

@ -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<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const { getKcContext } = createGetKcContext({
mockData
});
const { kcContext } = getKcContext({ mockPageId });
return { kcContext };
}

View File

@ -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 { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -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<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<
`${string}.ftl`,
Record<string, unknown>
>
>(params: {
kcContextExtraProperties: KcContextExtraProperties;
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
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<KcContext, { pageId: PageId }>>;
}): Extract<KcContext, { pageId: PageId }> {
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 };
}

View File

@ -1 +1,2 @@
export type { KcContext } from "./KcContext"; export type { ExtendKcContext, KcContext } from "./KcContext";
export { createGetKcContextMock } from "./getKcContextMock";

View File

@ -288,13 +288,11 @@ function decodeHtmlEntities(htmlStr){
are_same_path(path, []) are_same_path(path, [])
) || ( ) || (
<#-- attributesByName adds a lot of noise to the output and is not needed --> <#-- 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" && 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"]) are_same_path(path, ["register"])
) )
> >

View File

@ -3,9 +3,12 @@ import Fallback from "keycloakify/login/Fallback";
export default Fallback; export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms"; 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 { 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"; export type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -3,14 +3,28 @@ import type {
LoginThemePageId, LoginThemePageId,
nameOfTheLocalizationRealmOverridesUserProfileProperty nameOfTheLocalizationRealmOverridesUserProfileProperty
} from "keycloakify/bin/shared/constants"; } 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 { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n"; import type { MessageKey } from "../i18n/i18n";
type ExtractAfterStartingWith< export type ExtendKcContext<
Prefix extends string, KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
StrEnum KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
> = StrEnum extends `${Prefix}${infer U}` ? U : never; > = ValueOf<{
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
KcContext,
{ pageId: PageId }
> extends never
? KcContext.Common &
KcContextExtraProperties & {
pageId: PageId;
} & KcContextExtraPropertiesPerPage[PageId]
: Extract<KcContext, { pageId: PageId }> &
KcContextExtraProperties &
KcContextExtraPropertiesPerPage[PageId];
}>;
/** Take theses type definition with a grain of salt. /** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages. * Some values might be undefined on some pages.
@ -138,12 +152,12 @@ export declare namespace KcContext {
getFirstError: (...fieldNames: string[]) => string; getFirstError: (...fieldNames: string[]) => string;
}; };
properties: Record<string, string | undefined>;
authenticationSession?: { authenticationSession?: {
authSessionId: string; authSessionId: string;
tabId: string; tabId: string;
ssoLoginInOtherTabsUrl: string; ssoLoginInOtherTabsUrl: string;
}; };
properties: {};
__localizationRealmOverridesUserProfile?: Record<string, string>; __localizationRealmOverridesUserProfile?: Record<string, string>;
}; };
@ -585,7 +599,7 @@ export declare namespace KcContext {
} }
export type UserProfile = { export type UserProfile = {
attributes: Attribute[]; attributesByName: Record<string, Attribute>;
html5DataAnnotations?: Record<string, string>; html5DataAnnotations?: Record<string, string>;
}; };
@ -683,31 +697,31 @@ export type Attribute = {
| "photo"; | "photo";
}; };
export type Validators = Partial<{ export type Validators = {
length: Validators.DoIgnoreEmpty & Validators.Range; length?: Validators.DoIgnoreEmpty & Validators.Range;
integer: Validators.DoIgnoreEmpty & Validators.Range; integer?: Validators.DoIgnoreEmpty & Validators.Range;
email: Validators.DoIgnoreEmpty; email?: Validators.DoIgnoreEmpty;
pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string }; pattern?: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string };
options: Validators.Options; options?: Validators.Options;
multivalued: Validators.DoIgnoreEmpty & Validators.Range; multivalued?: Validators.DoIgnoreEmpty & Validators.Range;
// NOTE: Following are the validators for which we don't implement client side validation yet // 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. // or for which the validation can't be performed on the client side.
/* /*
double: Validators.DoIgnoreEmpty & Validators.Range; double?: Validators.DoIgnoreEmpty & Validators.Range;
"up-immutable-attribute": {}; "up-immutable-attribute"?: {};
"up-attribute-required-by-metadata-value": {}; "up-attribute-required-by-metadata-value"?: {};
"up-username-has-value": {}; "up-username-has-value"?: {};
"up-duplicate-username": {}; "up-duplicate-username"?: {};
"up-username-mutation": {}; "up-username-mutation"?: {};
"up-email-exists-as-username": {}; "up-email-exists-as-username"?: {};
"up-blank-attribute-value": Validators.ErrorMessage & { "fail-on-null": boolean; }; "up-blank-attribute-value"?: Validators.ErrorMessage & { "fail-on-null": boolean; };
"up-duplicate-email": {}; "up-duplicate-email"?: {};
"local-date": Validators.DoIgnoreEmpty; "local-date"?: Validators.DoIgnoreEmpty;
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; "person-name-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage;
uri: Validators.DoIgnoreEmpty; uri?: Validators.DoIgnoreEmpty;
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; "username-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage;
*/ */
}>; };
export declare namespace Validators { export declare namespace Validators {
export type DoIgnoreEmpty = { export type DoIgnoreEmpty = {

View File

@ -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<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
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<ExtendKcContext<KcContextExtension>> = {};
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.Register>(kcContext).profile.attributes = [];
const partialAttributes = [
...((
partialKcContextCustomMock as DeepPartial<KcContext.Register>
).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.Register>(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.Register>(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 };
}

View File

@ -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<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const { getKcContext } = createGetKcContext<KcContextExtension>({
mockData
});
const { kcContext } = getKcContext({ mockPageId });
return { kcContext };
}

View File

@ -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 { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -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<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<
`${string}.ftl`,
Record<string, unknown>
>
>(params: {
kcContextExtraProperties: KcContextExtraProperties;
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
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<KcContext, { pageId: PageId }>>;
}): Extract<KcContext, { pageId: PageId }> {
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 };
}

View File

@ -1 +1,7 @@
export type { KcContext } from "./KcContext"; export type {
ExtendKcContext,
KcContext,
Attribute,
PasswordPolicies
} from "./KcContext";
export { createGetKcContextMock } from "./getKcContextMock";

View File

@ -9,7 +9,8 @@ import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { BASE_URL } from "keycloakify/lib/BASE_URL"; import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [ const attributesByName = Object.fromEntries(
id<Attribute[]>([
{ {
validators: { validators: {
length: { length: {
@ -73,7 +74,8 @@ const attributes: Attribute[] = [
readOnly: false, readOnly: false,
name: "lastName" name: "lastName"
} }
]; ]).map(attribute => [attribute.name, attribute])
);
const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`; const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
@ -265,7 +267,7 @@ export const kcContextMocks = [
recaptchaRequired: false, recaptchaRequired: false,
pageId: "register.ftl", pageId: "register.ftl",
profile: { profile: {
attributes attributesByName
}, },
scripts: [ scripts: [
//"https://www.google.com/recaptcha/api.js" //"https://www.google.com/recaptcha/api.js"
@ -416,7 +418,7 @@ export const kcContextMocks = [
...kcContextCommonMock, ...kcContextCommonMock,
pageId: "login-update-profile.ftl", pageId: "login-update-profile.ftl",
profile: { profile: {
attributes attributesByName
} }
}), }),
id<KcContext.LoginIdpLinkConfirm>({ id<KcContext.LoginIdpLinkConfirm>({
@ -472,14 +474,16 @@ export const kcContextMocks = [
...kcContextCommonMock, ...kcContextCommonMock,
pageId: "idp-review-user-profile.ftl", pageId: "idp-review-user-profile.ftl",
profile: { profile: {
attributes attributesByName
} }
}), }),
id<KcContext.UpdateEmail>({ id<KcContext.UpdateEmail>({
...kcContextCommonMock, ...kcContextCommonMock,
pageId: "update-email.ftl", pageId: "update-email.ftl",
profile: { profile: {
attributes: attributes.filter(attribute => attribute.name === "email") attributesByName: {
email: attributesByName["email"]
}
} }
}), }),
id<KcContext.SelectAuthenticator>({ id<KcContext.SelectAuthenticator>({

View File

@ -9,7 +9,7 @@ import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/Kc
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { formatNumber } from "keycloakify/tools/formatNumber"; import { formatNumber } from "keycloakify/tools/formatNumber";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { structuredCloneButFunctions } from "tools/structuredCloneButFunctions"; import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export type FormFieldError = { export type FormFieldError = {
@ -68,7 +68,7 @@ export type FormAction =
export type KcContextLike = { export type KcContextLike = {
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">; messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
profile: { profile: {
attributes: Attribute[]; attributesByName: Record<string, Attribute>;
html5DataAnnotations?: Record<string, string>; html5DataAnnotations?: Record<string, string>;
}; };
passwordRequired?: boolean; passwordRequired?: boolean;
@ -137,7 +137,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const attributes = (() => { const attributes = (() => {
retrocompat_patch: { 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; break retrocompat_patch;
} }
@ -217,7 +217,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
assert(false, "Unable to mock user profile from the current kcContext"); 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 !== "") { if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
attribute_pre_group_patch as Attribute & { attribute_pre_group_patch as Attribute & {

View File

@ -1,31 +0,0 @@
export type AndByDiscriminatingKey<
DiscriminatingKey extends string,
U1 extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> = AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
export declare namespace AndByDiscriminatingKey {
export type Tf1<
DiscriminatingKey extends string,
U1,
U1Again extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
U1 extends Pick<U2, DiscriminatingKey>
? Tf2<DiscriminatingKey, U1, U2, U1Again>
: U1Again[DiscriminatingKey] & U2[DiscriminatingKey] extends never
? U1 | U2
: U1;
export type Tf2<
DiscriminatingKey extends string,
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> =
U2 extends Pick<SingletonU1, DiscriminatingKey>
? U2 & SingletonU1
: U2 extends Pick<U1, DiscriminatingKey>
? never
: U2;
}

View File

@ -0,0 +1,4 @@
export type ExtractAfterStartingWith<
Prefix extends string,
StrEnum
> = StrEnum extends `${Prefix}${infer U}` ? U : never;

2
src/tools/ValueOf.ts Normal file
View File

@ -0,0 +1,2 @@
/** Pendant of `keyof T` */
export type ValueOf<T> = T[keyof T];

View File

@ -2,44 +2,60 @@ import { assert } from "tsafe/assert";
import { is } from "tsafe/is"; import { is } from "tsafe/is";
import { structuredCloneButFunctions } from "./structuredCloneButFunctions"; 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: { export function deepAssign(params: {
target: Record<string, unknown>; target: Record<string, unknown>;
source: Record<string, unknown>; source: Record<string, unknown>;
}) { }): void {
const { target } = params; const { target, source } = params;
const source = structuredCloneButFunctions(params.source);
Object.keys(source).forEach(key => { Object.keys(source).forEach(key => {
var dereferencedSource = source[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 ( if (
target[key] === undefined ||
dereferencedSource instanceof Function || dereferencedSource instanceof Function ||
!(dereferencedSource instanceof Object) !(dereferencedSource instanceof Object)
) { ) {
Object.defineProperty(target, key, { assign({
enumerable: true, target,
writable: true, key,
configurable: true,
value: dereferencedSource value: dereferencedSource
}); });
return; return;
} }
const dereferencedTarget = target[key]; if (!(target[key] instanceof Object)) {
target[key] = {};
if (dereferencedSource instanceof Array) {
assert(is<unknown[]>(dereferencedTarget));
assert(is<unknown[]>(dereferencedSource));
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
return;
} }
const dereferencedTarget = target[key];
assert(is<Record<string, unknown>>(dereferencedTarget)); assert(is<Record<string, unknown>>(dereferencedTarget));
assert(is<Record<string, unknown>>(dereferencedSource)); assert(is<Record<string, unknown>>(dereferencedSource));
@ -49,3 +65,18 @@ export function deepAssign(params: {
}); });
}); });
} }
function assign(params: {
target: Record<string, unknown>;
key: string;
value: unknown;
}): void {
const { target, key, value } = params;
Object.defineProperty(target, key, {
enumerable: true,
writable: true,
configurable: true,
value
});
}

View File

@ -1,19 +1,31 @@
import React from "react"; import React from "react";
import { getKcContext, type KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import { getKcContextMock } from "./kcContextMock";
import KcApp from "./KcApp"; import KcApp from "./KcApp";
import type { DeepPartial } from "../../dist/tools/DeepPartial"; import type { DeepPartial } from "../../dist/tools/DeepPartial";
export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) { export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
const { pageId } = params; const { pageId } = params;
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) { function PageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
const { kcContext } = getKcContext({ const { kcContext: overrides } = props;
mockPageId: pageId,
storyPartialKcContext: params.kcContext const kcContextMock = getKcContextMock({
pageId,
overrides
}); });
return <KcApp kcContext={kcContext} />; return <KcApp kcContext={kcContextMock} />;
} }
return { PageStory }; return { PageStory };
} }
export const parameters = {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
};

View File

@ -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<typeof kcContext>; export type KcContext = ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>;

View File

@ -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
});

View File

@ -1,4 +1,4 @@
import { ExtendKcContext } from "../../dist/login"; import type { ExtendKcContext } from "../../dist/login";
export type KcContextExtraProperties = {}; export type KcContextExtraProperties = {};

View File

@ -1,5 +1,8 @@
import { createGetKcContextMock } from "../../dist/login"; import { createGetKcContextMock } from "../../dist/login";
import { KcContextExtraProperties, KcContextExtraPropertiesPerPage } from "./kcContext"; import type {
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
} from "./kcContext";
const kcContextExtraProperties: KcContextExtraProperties = {}; const kcContextExtraProperties: KcContextExtraProperties = {};
const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {}; const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {};

View File

@ -7,7 +7,6 @@ import {
import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode"; import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode";
import { same } from "evt/tools/inDepth/same"; import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
import { isSameCode } from "../tools/isSameCode";
import { import {
basenameOfTheKeycloakifyResourcesDir, basenameOfTheKeycloakifyResourcesDir,
nameOfTheGlobal 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);
}

View File

@ -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<KcContextExtension>["pageId"];
}) => {
const { mockPageId } = params;
const { getKcContext } = createGetKcContext<KcContextExtension>({
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<Equals<typeof kcContext, KcContext.Login>>();
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<Equals<typeof kcContext, KcContext.Common & { pageId: typeof pageId }>>();
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<Equals<typeof kcContext, KcContext.Login>>();
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<Equals<typeof kcContext, KcContext | undefined>>();
assert(kcContext === undefined);
});
it("mock are properly overwritten", () => {
const { getKcContext } = createGetKcContext();
const displayName = "myDisplayName";
const { kcContext } = getKcContext({
mockPageId: "login.ftl",
storyPartialKcContext: {
realm: {
displayName
}
}
});
assert<Equals<typeof kcContext, KcContext.Login>>();
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<Equals<typeof kcContext, KcContext.Login | KcContext.Register>>();
});
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<Equals<typeof kcContext, KcContext>>();
});
});

View File

@ -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<KcContextExtension>["pageId"];
}) => {
const { mockPageId } = params;
const { kcContext } = getKcContext<KcContextExtension>({
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<Equals<typeof kcContext, KcContext.Login>>();
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<Equals<typeof kcContext, KcContext.Common & { pageId: typeof pageId }>>();
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<Equals<typeof kcContext, KcContext | undefined>>();
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<Equals<typeof kcContext, KcContext | undefined>>();
assert(kcContext === undefined);
});
});

View File

@ -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<Equals<Got, Expected>>();
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<Equals<Got, Expected>>();
}

View File

@ -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<KcContext, { pageId: "login.ftl" }>;
type Expected = KcContextBase.Login & {
properties: { myCustomProperty: string | undefined };
} & { foo: string };
assert<Equals<Got, Expected>>();
}
{
type Got = Extract<KcContext, { pageId: "register.ftl" }>;
type Expected = KcContextBase.Register & {
properties: { myCustomProperty: string | undefined };
};
assert<Equals<Got, Expected>>();
}
{
type Got = Extract<KcContext, { pageId: "my-custom-page.ftl" }>;
type Expected = KcContextBase.Common &
KcContextExtraProperties & { pageId: "my-custom-page.ftl" } & {
properties: { myCustomProperty: string | undefined };
} & { bar: number };
assert<Got extends Expected ? true : false>();
assert<Expected extends Got ? true : false>();
}
const { getKcContextMock } = createGetKcContextMock({
kcContextExtraProperties: Reflect<KcContextExtraProperties>(),
kcContextExtraPropertiesPerPage: Reflect<KcContextExtraPropertiesPerPage>()
});
{
const got = getKcContextMock({
pageId: "login.ftl"
});
type Expected = Extract<KcContext, { pageId: "login.ftl" }>;
assert<Equals<typeof got, Expected>>();
}
{
const got = getKcContextMock({
pageId: "register.ftl"
});
type Expected = Extract<KcContext, { pageId: "register.ftl" }>;
assert<Equals<typeof got, Expected>>();
}
{
const got = getKcContextMock({
pageId: "my-custom-page.ftl"
});
type Expected = Extract<KcContext, { pageId: "my-custom-page.ftl" }>;
assert<Equals<typeof got, Expected>>();
}
getKcContextMock({
// @ts-expect-error
pageId: "non-existing-page.ftl"
});
getKcContextMock({
pageId: "login.ftl",
overrides: {
// @ts-expect-error
bar: 42
}
});
createGetKcContextMock({
kcContextExtraProperties: Reflect<KcContextExtraProperties>(),
kcContextExtraPropertiesPerPage: Reflect<KcContextExtraPropertiesPerPage>(),
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<KcContextExtraProperties>(),
kcContextExtraPropertiesPerPage: Reflect<KcContextExtraPropertiesPerPage>(),
overridesPerPage: {
"register.ftl": {
// @ts-expect-error
nonExistingProperty: 42
}
}
});
}
{
type KcContextExtraProperties = {};
type KcContextExtraPropertiesPerPage = {};
type KcContext = ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>;
{
type Got = Extract<KcContext, { pageId: "login.ftl" }>;
type Expected = KcContextBase.Login;
assert<Equals<Got, Expected>>();
}
{
type Got = Extract<KcContext, { pageId: "register.ftl" }>;
type Expected = KcContextBase.Register;
assert<Equals<Got, Expected>>();
}
const { getKcContextMock } = createGetKcContextMock({
kcContextExtraProperties: Reflect<KcContextExtraProperties>(),
kcContextExtraPropertiesPerPage: Reflect<KcContextExtraPropertiesPerPage>()
});
{
const got = getKcContextMock({
pageId: "login.ftl"
});
type Expected = Extract<KcContext, { pageId: "login.ftl" }>;
assert<Equals<typeof got, Expected>>();
}
{
const got = getKcContextMock({
pageId: "register.ftl"
});
type Expected = Extract<KcContext, { pageId: "register.ftl" }>;
assert<Equals<typeof got, Expected>>();
}
getKcContextMock({
// @ts-expect-error
pageId: "non-existing-page.ftl"
});
getKcContextMock({
pageId: "login.ftl",
overrides: {
// @ts-expect-error
bar: 42
}
});
}

View File

@ -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<KcContextExtraProperties>({
properties: {
MY_ENV_VAR: "my env var value"
}
}),
kcContextExtraPropertiesPerPage: id<KcContextExtraPropertiesPerPage>({
"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<Attribute>({
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);
});
});

View File

@ -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);
}