Improve flow for testing e-mail server settings (#3619)

This commit is contained in:
Jon Koops 2022-10-24 13:43:46 +02:00 committed by GitHub
parent c56f9b4132
commit 1de4d24593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 356 additions and 416 deletions

View file

@ -78,7 +78,15 @@ describe("Realm settings tabs tests", () => {
});
it("Go to email tab", () => {
const msg: string = "Error! Failed to send email.";
// Configure an e-mail address so we can test the connection settings.
cy.wrap(null).then(async () => {
const adminUser = await adminClient.getAdminUser();
await adminClient.updateUser(adminUser.id!, {
email: "admin@example.com",
});
});
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-email-tab").click();
//required fields not filled in or not filled properly
@ -100,17 +108,10 @@ describe("Realm settings tabs tests", () => {
realmSettingsPage.toggleCheck(realmSettingsPage.enableSslCheck);
realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck);
realmSettingsPage.fillHostField("localhost");
cy.intercept(`/admin/realms/${realmName}/users/*`).as("load");
cy.findByTestId(realmSettingsPage.testConnectionButton).click();
cy.wait("@load");
//ln109-113 cause the tests to fail locally, but is needed for the test to pass on the dashboard.
realmSettingsPage.fillEmailField(
"example" + (Math.random() + 1).toString(36).substring(7) + "@example.com"
);
cy.findByTestId(realmSettingsPage.modalTestConnectionButton).click();
masthead.checkNotificationMessage(msg, true);
masthead.checkNotificationMessage("Error! Failed to send email", true);
});
it("Go to themes tab", () => {

View file

@ -4,11 +4,16 @@ export default class Masthead extends CommonElements {
private helpBtn = "#help";
private closeAlertMessageBtn = ".pf-c-alert__action button";
private closeLastAlertMessageBtn =
".pf-c-alert-group > li:first-child .pf-c-alert__action button";
"li:first-child .pf-c-alert__action button";
private alertMessage = ".pf-c-alert__title";
private userDrpDwn = "#user-dropdown";
private userDrpDwnKebab = "#user-dropdown-kebab";
private globalAlerts = "global-alerts";
private getAlertsContainer() {
return cy.findByTestId(this.globalAlerts);
}
checkIsAdminConsole() {
cy.get(this.logoBtn).should("exist");
@ -50,10 +55,13 @@ export default class Masthead extends CommonElements {
}
checkNotificationMessage(message: string, closeNotification = true) {
cy.get(this.alertMessage).should("contain.text", message);
this.getAlertsContainer()
.find(this.alertMessage)
.should("contain.text", message);
if (closeNotification) {
cy.get(`button[title="` + message.replaceAll('"', '\\"') + `"]`)
this.getAlertsContainer()
.find(`button[title="` + message.replaceAll('"', '\\"') + `"]`)
.last()
.click({ force: true });
}
@ -61,14 +69,16 @@ export default class Masthead extends CommonElements {
}
closeLastAlertMessage() {
cy.get(this.closeLastAlertMessageBtn).click();
this.getAlertsContainer().find(this.closeLastAlertMessageBtn).click();
return this;
}
closeAllAlertMessages() {
cy.get(this.closeAlertMessageBtn).each(() => {
cy.get(this.closeAlertMessageBtn).click({ force: true, multiple: true });
this.getAlertsContainer().find(this.closeAlertMessageBtn).click({
force: true,
multiple: true,
});
return this;
}

View file

@ -98,6 +98,17 @@ class AdminClient {
return await this.client.users.create(user);
}
async updateUser(id: string, payload: UserRepresentation) {
await this.login();
return this.client.users.update({ id }, payload);
}
async getAdminUser() {
await this.login();
const [user] = await this.client.users.find({ username: "admin" });
return user;
}
async addUserToGroup(userId: string, groupId: string) {
await this.login();
await this.client.users.addToGroup({ id: userId, groupId });

View file

@ -114,8 +114,6 @@
"loginWithEmailHelpText": "Allow users to log in with their email address.",
"duplicateEmailsAllowed": "Duplicate emails",
"duplicateEmailsHelpText": "Allow multiple users to have the same email address. Changing this setting will also clear the user's cache. It is recommended to manually update email constraints of existing users in the database after switching off support for duplicate email addresses.",
"provideEmailTitle": "Provide your email address",
"provideEmail": "To test connection, you should provide your email address first.",
"verifyEmail": "Verify email",
"verifyEmailHelpText": "Require user to verify their email address after initial login or after address changes are submitted.",
"userInfoSettings": "User info settings",
@ -123,8 +121,13 @@
"enableSwitchSuccess": "{{switch}} changed successfully",
"enableSwitchError": "Could not enable / disable due to {{error}}",
"testConnection": "Test connection",
"testConnectionHint": {
"withEmail": "When testing the connection an e-mail will be sent to the current user ({{email}}).",
"withoutEmail": "To test the connection you must first configure an e-mail address for the current user ({{userName}}).",
"withoutEmailAction": "Configure e-mail address"
},
"testConnectionSuccess": "Success! SMTP connection successful. E-mail was sent!",
"testConnectionError": "Error! Failed to send email.",
"testConnectionError": "Error! {{error}}",
"realmId": "Realm ID",
"displayName": "Display name",
"htmlDisplayName": "HTML Display name",

View file

@ -13,7 +13,7 @@ type AlertPanelProps = {
export function AlertPanel({ alerts, onCloseAlert }: AlertPanelProps) {
return (
<AlertGroup isToast>
<AlertGroup data-testid="global-alerts" isToast>
{alerts.map(({ id, variant, message, description }) => (
<Alert
key={id}

View file

@ -1,107 +0,0 @@
import {
Button,
ButtonVariant,
Form,
FormGroup,
Modal,
ModalVariant,
TextContent,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { emailRegexPattern } from "../util";
import { useAdminClient } from "../context/auth/AdminClient";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import type { EmailRegistrationCallback } from "./EmailTab";
type AddUserEmailModalProps = {
callback: EmailRegistrationCallback;
};
type AddUserEmailForm = {
email: string;
};
export const AddUserEmailModal = ({ callback }: AddUserEmailModalProps) => {
const { t } = useTranslation("groups");
const { adminClient } = useAdminClient();
const { whoAmI } = useWhoAmI();
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<AddUserEmailForm>({
defaultValues: { email: "" },
});
const watchEmailInput = watch("email", "");
const cancel = () => callback(false);
const proceed = () => callback(true);
const save = async (formData: AddUserEmailForm) => {
await adminClient.users.update({ id: whoAmI.getUserId() }, formData);
proceed();
};
return (
<Modal
variant={ModalVariant.small}
title={t("realm-settings:provideEmailTitle")}
isOpen={true}
onClose={cancel}
actions={[
<Button
data-testid="modal-test-connection-button"
key="confirm"
variant="primary"
type="submit"
form="email-form"
isDisabled={!watchEmailInput}
>
{t("common:testConnection")}
</Button>,
<Button
id="modal-cancel"
data-testid="cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={cancel}
>
{t("common:cancel")}
</Button>,
]}
>
<TextContent className="kc-provide-email-text">
{t("realm-settings:provideEmail")}
</TextContent>
<Form id="email-form" isHorizontal onSubmit={handleSubmit(save)}>
<FormGroup
className="kc-email-form-group"
name="add-email-address"
fieldId="email-id"
helperTextInvalid={t("users:emailInvalid")}
validated={
errors.email ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
data-testid="email-address-input"
ref={register({ required: true, pattern: emailRegexPattern })}
autoFocus
type="text"
id="add-email"
name="email"
validated={
errors.email ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
</Form>
</Modal>
);
};

View file

@ -1,5 +1,9 @@
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import {
ActionGroup,
ActionListItem,
Alert,
AlertActionLink,
AlertVariant,
Button,
Checkbox,
@ -7,29 +11,29 @@ import {
PageSection,
Switch,
} from "@patternfly/react-core";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom-v5-compat";
import { useAlerts } from "../components/alert/Alerts";
import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormPanel } from "../components/scroll-form/FormPanel";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { PasswordInput } from "../components/password-input/PasswordInput";
import { FormPanel } from "../components/scroll-form/FormPanel";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { toUser } from "../user/routes/User";
import { emailRegexPattern } from "../util";
import { AddUserEmailModal } from "./AddUserEmailModal";
import { PasswordInput } from "../components/password-input/PasswordInput";
import { useCurrentUser } from "../utils/useCurrentUser";
import "./realm-settings-section.css";
type RealmSettingsEmailTabProps = {
realm: RealmRepresentation;
};
export type EmailRegistrationCallback = (registered: boolean) => void;
export const RealmSettingsEmailTab = ({
realm: initialRealm,
}: RealmSettingsEmailTabProps) => {
@ -37,10 +41,9 @@ export const RealmSettingsEmailTab = ({
const { adminClient } = useAdminClient();
const { realm: realmName } = useRealm();
const { addAlert, addError } = useAlerts();
const { whoAmI } = useWhoAmI();
const currentUser = useCurrentUser();
const [realm, setRealm] = useState(initialRealm);
const [callback, setCallback] = useState<EmailRegistrationCallback>();
const {
register,
control,
@ -63,12 +66,6 @@ export const RealmSettingsEmailTab = ({
const save = async (form: RealmRepresentation) => {
try {
const registered = await registerEmailIfNeeded();
if (!registered) {
return;
}
const savedRealm = { ...realm, ...form };
// For default value, back end is expecting null instead of empty string
@ -102,12 +99,6 @@ export const RealmSettingsEmailTab = ({
if (serverSettings.port === 0) serverSettings.port = null;
try {
const registered = await registerEmailIfNeeded();
if (!registered) {
return;
}
await adminClient.realms.testSMTPConnection(
{ realm: realm.realm! },
serverSettings
@ -118,36 +109,7 @@ export const RealmSettingsEmailTab = ({
}
};
/**
* Triggers the flow to register the user's email if the user does not yet have one configured, if successful resolves true, otherwise false.
*/
const registerEmailIfNeeded = async () => {
const user = await adminClient.users.findOne({ id: whoAmI.getUserId() });
// A user should always be found, throw if it is not.
if (!user) {
throw new Error("Unable to find user.");
}
// User already has an e-mail associated with it, no need to register.
if (user.email) {
return true;
}
// User needs to register, show modal to do so.
return new Promise<boolean>((resolve) => {
const callback: EmailRegistrationCallback = (registered) => {
setCallback(undefined);
resolve(registered);
};
setCallback(() => callback);
});
};
return (
<>
{callback && <AddUserEmailModal callback={callback} />}
<PageSection variant="light">
<FormPanel title={t("template")} className="kc-email-template">
<FormAccess
@ -378,9 +340,7 @@ export const RealmSettingsEmailTab = ({
data-testid="password-input"
name="smtpServer.password"
aria-label={t("password")}
validated={
errors.smtpServer?.password ? "error" : "default"
}
validated={errors.smtpServer?.password ? "error" : "default"}
ref={register({ required: true })}
/>
</FormGroup>
@ -388,6 +348,7 @@ export const RealmSettingsEmailTab = ({
)}
<ActionGroup>
<ActionListItem>
<Button
variant="primary"
type="submit"
@ -395,16 +356,22 @@ export const RealmSettingsEmailTab = ({
>
{t("common:save")}
</Button>
</ActionListItem>
<ActionListItem>
<Button
variant="secondary"
onClick={() => testConnection()}
data-testid="test-connection-button"
isDisabled={
!(emailRegexPattern.test(watchFromValue) && watchHostValue)
!(emailRegexPattern.test(watchFromValue) && watchHostValue) ||
!currentUser?.email
}
aria-describedby="descriptionTestConnection"
>
{t("common:testConnection")}
</Button>
</ActionListItem>
<ActionListItem>
<Button
variant="link"
onClick={reset}
@ -412,10 +379,47 @@ export const RealmSettingsEmailTab = ({
>
{t("common:revert")}
</Button>
</ActionListItem>
</ActionGroup>
{currentUser && (
<FormGroup id="descriptionTestConnection">
{currentUser.email ? (
<Alert
variant="info"
isInline
title={t("testConnectionHint.withEmail", {
email: currentUser.email,
})}
/>
) : (
<Alert
variant="warning"
isInline
title={t("testConnectionHint.withoutEmail", {
userName: currentUser.username,
})}
actionLinks={
<AlertActionLink
component={(props) => (
<Link
{...props}
to={toUser({
realm: realmName,
id: currentUser.id!,
tab: "settings",
})}
/>
)}
>
{t("testConnectionHint.withoutEmailAction")}
</AlertActionLink>
}
/>
)}
</FormGroup>
)}
</FormAccess>
</FormPanel>
</PageSection>
</>
);
};

View file

@ -0,0 +1,18 @@
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { useState } from "react";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useWhoAmI } from "../context/whoami/WhoAmI";
export function useCurrentUser() {
const { whoAmI } = useWhoAmI();
const { adminClient } = useAdminClient();
const [currentUser, setCurrentUser] = useState<UserRepresentation>();
const userId = whoAmI.getUserId();
useFetch(() => adminClient.users.findOne({ id: userId }), setCurrentUser, [
userId,
]);
return currentUser;
}