Refactor kcContext, avoid having mocks in the dist https://github.com/keycloakify/keycloakify/discussions/299#discussioncomment-9616747
This commit is contained in:
parent
37a060c4db
commit
c1a63edd71
@ -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 = (() => {
|
||||
|
@ -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";
|
||||
|
@ -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<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 =
|
||||
| KcContext.Password
|
||||
|
@ -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 };
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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];
|
||||
}
|
80
src/account/kcContext/getKcContextMock.ts
Normal file
80
src/account/kcContext/getKcContextMock.ts
Normal 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 };
|
||||
}
|
@ -1 +1,2 @@
|
||||
export type { KcContext } from "./KcContext";
|
||||
export type { ExtendKcContext, KcContext } from "./KcContext";
|
||||
export { createGetKcContextMock } from "./getKcContextMock";
|
||||
|
@ -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"])
|
||||
)
|
||||
>
|
||||
|
@ -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";
|
||||
|
@ -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<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];
|
||||
}>;
|
||||
|
||||
/** 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<string, string | undefined>;
|
||||
authenticationSession?: {
|
||||
authSessionId: string;
|
||||
tabId: string;
|
||||
ssoLoginInOtherTabsUrl: string;
|
||||
};
|
||||
properties: {};
|
||||
__localizationRealmOverridesUserProfile?: Record<string, string>;
|
||||
};
|
||||
|
||||
@ -585,7 +599,7 @@ export declare namespace KcContext {
|
||||
}
|
||||
|
||||
export type UserProfile = {
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
html5DataAnnotations?: Record<string, string>;
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
|
@ -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 };
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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];
|
||||
}
|
80
src/login/kcContext/getKcContextMock.ts
Normal file
80
src/login/kcContext/getKcContextMock.ts
Normal 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 };
|
||||
}
|
@ -1 +1,7 @@
|
||||
export type { KcContext } from "./KcContext";
|
||||
export type {
|
||||
ExtendKcContext,
|
||||
KcContext,
|
||||
Attribute,
|
||||
PasswordPolicies
|
||||
} from "./KcContext";
|
||||
export { createGetKcContextMock } from "./getKcContextMock";
|
||||
|
@ -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<Attribute[]>([
|
||||
{
|
||||
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<KcContext.LoginIdpLinkConfirm>({
|
||||
@ -472,14 +474,16 @@ export const kcContextMocks = [
|
||||
...kcContextCommonMock,
|
||||
pageId: "idp-review-user-profile.ftl",
|
||||
profile: {
|
||||
attributes
|
||||
attributesByName
|
||||
}
|
||||
}),
|
||||
id<KcContext.UpdateEmail>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "update-email.ftl",
|
||||
profile: {
|
||||
attributes: attributes.filter(attribute => attribute.name === "email")
|
||||
attributesByName: {
|
||||
email: attributesByName["email"]
|
||||
}
|
||||
}
|
||||
}),
|
||||
id<KcContext.SelectAuthenticator>({
|
||||
|
@ -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<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
html5DataAnnotations?: Record<string, string>;
|
||||
};
|
||||
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 & {
|
||||
|
@ -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;
|
||||
}
|
4
src/tools/ExtractAfterStartingWith.ts
Normal file
4
src/tools/ExtractAfterStartingWith.ts
Normal 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
2
src/tools/ValueOf.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/** Pendant of `keyof T` */
|
||||
export type ValueOf<T> = T[keyof T];
|
@ -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<string, unknown>;
|
||||
source: Record<string, unknown>;
|
||||
}) {
|
||||
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<unknown[]>(dereferencedTarget));
|
||||
assert(is<unknown[]>(dereferencedSource));
|
||||
|
||||
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
|
||||
|
||||
return;
|
||||
if (!(target[key] instanceof Object)) {
|
||||
target[key] = {};
|
||||
}
|
||||
|
||||
const dereferencedTarget = target[key];
|
||||
|
||||
assert(is<Record<string, unknown>>(dereferencedTarget));
|
||||
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
|
||||
});
|
||||
}
|
||||
|
@ -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<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
|
||||
const { kcContext } = getKcContext({
|
||||
mockPageId: pageId,
|
||||
storyPartialKcContext: params.kcContext
|
||||
function PageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return <KcApp kcContext={kcContext} />;
|
||||
return <KcApp kcContext={kcContextMock} />;
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
}
|
||||
|
||||
export const parameters = {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
>;
|
||||
|
13
stories/account/kcContextMock.ts
Normal file
13
stories/account/kcContextMock.ts
Normal 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
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { ExtendKcContext } from "../../dist/login";
|
||||
import type { ExtendKcContext } from "../../dist/login";
|
||||
|
||||
export type KcContextExtraProperties = {};
|
||||
|
||||
|
@ -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 = {};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>>();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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>>();
|
||||
}
|
211
test/login/kcContext.typelevel-spec.ts
Normal file
211
test/login/kcContext.typelevel-spec.ts
Normal 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
|
||||
}
|
||||
});
|
||||
}
|
202
test/login/kcContextMock.spec.ts
Normal file
202
test/login/kcContextMock.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user