From e9229f7e25951b629ee390a53d6a66c9a2e430e5 Mon Sep 17 00:00:00 2001 From: Shane Findley Date: Mon, 2 Sep 2024 15:49:00 +0200 Subject: [PATCH] SONAR 22666 Edit email configuration (#11638) --- .../main/js/api/mocks/SystemServiceMock.ts | 1 + .../AuthenticationSelector.tsx | 22 +- .../email-notification/EmailNotification.tsx | 15 +- .../EmailNotificationConfiguration.tsx | 38 +- .../EmailNotificationFormField.tsx | 150 +++- .../EmailNotificationOverview.tsx | 117 ++++ .../__tests__/EmailNotification-it.tsx | 151 +++- .../__tests__/utils-test.ts | 648 ++++++++++++++++++ .../components/email-notification/utils.ts | 71 +- server/sonar-web/src/main/js/types/system.ts | 37 +- .../resources/org/sonar/l10n/core.properties | 8 +- 11 files changed, 1164 insertions(+), 94 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationOverview.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/utils-test.ts diff --git a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts index 23da678af86..2476e88b6bb 100644 --- a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts @@ -67,6 +67,7 @@ export default class SystemServiceMock { jest.mocked(getSystemUpgrades).mockImplementation(this.handleGetSystemUpgrades); jest.mocked(getEmailConfigurations).mockImplementation(this.handleGetEmailConfigurations); jest.mocked(postEmailConfiguration).mockImplementation(this.handlePostEmailConfiguration); + jest.mocked(patchEmailConfiguration).mockImplementation(this.handlePatchEmailConfiguration); } handleGetSystemInfo = () => { diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx index 05b65957c40..ece46a15cb8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx @@ -57,6 +57,7 @@ export function AuthenticationSelector(props: Readonly {translate('email_notification.form.oauth_auth.description')} + {translate('email_notification.form.oauth_auth.supported')} @@ -77,33 +78,30 @@ export function AuthenticationSelector(props: Readonly onChange({ oauthAuthenticationHost: value })} name={translate('email_notification.form.oauth_authentication_host')} required - value={ - configuration.authMethod === AuthMethod.OAuth - ? configuration.oauthAuthenticationHost - : '' - } + value={configuration.oauthAuthenticationHost ?? ''} /> onChange({ oauthClientId: value })} name={translate('email_notification.form.oauth_client_id')} required type="password" - value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientId : ''} + value={configuration.oauthClientId ?? ''} /> onChange({ oauthClientSecret: value })} name={translate('email_notification.form.oauth_client_secret')} required + requiresRevaluation type="password" - value={ - configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientSecret : '' - } + value={configuration.oauthClientSecret ?? ''} /> onChange({ oauthTenant: value })} name={translate('email_notification.form.oauth_tenant')} required - value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthTenant : ''} + value={configuration.oauthTenant ?? ''} /> ) : ( onChange({ basicPassword: value })} name={translate('email_notification.form.basic_password')} required + requiresRevaluation type="password" - value={configuration.authMethod === AuthMethod.Basic ? configuration.basicPassword : ''} + value={configuration.basicPassword ?? ''} /> )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx index 2895abd0f8f..2c43f2532b3 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx @@ -23,8 +23,10 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { useGetEmailConfiguration } from '../../../../queries/system'; import EmailNotificationConfiguration from './EmailNotificationConfiguration'; +import EmailNotificationOverview from './EmailNotificationOverview'; export default function EmailNotification() { + const [isEditing, setIsEditing] = React.useState(false); const { data: configuration, isLoading } = useGetEmailConfiguration(); return ( @@ -34,7 +36,18 @@ export default function EmailNotification() { - + {configuration == null || isEditing ? ( + setIsEditing(false)} + onSubmitted={() => setIsEditing(false)} + /> + ) : ( + setIsEditing(true)} + emailConfiguration={configuration} + /> + )} ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx index 48f98ff7085..929ec4946a9 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx @@ -19,6 +19,7 @@ */ import { Button, ButtonVariety } from '@sonarsource/echoes-react'; import { NumberedList, NumberedListItem } from 'design-system/lib'; +import { noop } from 'lodash'; import React, { useCallback, useEffect } from 'react'; import { translate } from '../../../../helpers/l10n'; import { @@ -29,10 +30,12 @@ import { AuthMethod, EmailConfiguration } from '../../../../types/system'; import { AuthenticationSelector } from './AuthenticationSelector'; import { CommonSMTP } from './CommonSMTP'; import { SenderInformation } from './SenderInformation'; -import { checkEmailConfigurationValidity } from './utils'; +import { checkEmailConfigurationHasChanges } from './utils'; interface Props { emailConfiguration: EmailConfiguration | null; + onCancel: () => void; + onSubmitted: () => void; } const FORM_ID = 'email-notifications'; @@ -49,7 +52,7 @@ const EMAIL_CONFIGURATION_DEFAULT: EmailConfiguration = { }; export default function EmailNotificationConfiguration(props: Readonly) { - const { emailConfiguration } = props; + const { emailConfiguration, onCancel, onSubmitted } = props; const [canSave, setCanSave] = React.useState(false); const [newConfiguration, setNewConfiguration] = React.useState( @@ -59,16 +62,20 @@ export default function EmailNotificationConfiguration(props: Readonly) { const { mutateAsync: saveEmailConfiguration } = useSaveEmailConfigurationMutation(); const { mutateAsync: updateEmailConfiguration } = useUpdateEmailConfigurationMutation(); + const hasConfiguration = emailConfiguration !== undefined; + const onChange = useCallback( (newValue: Partial) => { const newConfig = { ...newConfiguration, ...newValue, }; - setCanSave(checkEmailConfigurationValidity(newConfig as EmailConfiguration)); + setCanSave( + checkEmailConfigurationHasChanges(newConfig as EmailConfiguration, emailConfiguration), + ); setNewConfiguration(newConfig as EmailConfiguration); }, - [newConfiguration, setNewConfiguration], + [emailConfiguration, newConfiguration], ); const onSubmit = useCallback( @@ -92,18 +99,24 @@ export default function EmailNotificationConfiguration(props: Readonly) { ...authConfiguration, }; - if (newConfiguration?.id === undefined) { - saveEmailConfiguration(newEmailConfiguration); + if (emailConfiguration?.id === undefined) { + saveEmailConfiguration(newEmailConfiguration).then(() => onSubmitted(), noop); } else { - newEmailConfiguration.id = newConfiguration.id; updateEmailConfiguration({ emailConfiguration: newEmailConfiguration, - id: newEmailConfiguration.id, - }); + id: emailConfiguration.id, + }).then(() => onSubmitted(), noop); } } }, - [canSave, newConfiguration, saveEmailConfiguration, updateEmailConfiguration], + [ + canSave, + emailConfiguration, + onSubmitted, + newConfiguration, + saveEmailConfiguration, + updateEmailConfiguration, + ], ); useEffect(() => { @@ -142,6 +155,11 @@ export default function EmailNotificationConfiguration(props: Readonly) { > {translate('email_notification.form.save_configuration')} + {hasConfiguration && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx index d881be0d2d8..e5111eab702 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx @@ -17,10 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { InputSize, Select } from '@sonarsource/echoes-react'; +import { + ButtonIcon, + ButtonVariety, + IconDelete, + IconEdit, + InputSize, + Select, +} from '@sonarsource/echoes-react'; import { FormField, InputField, TextError } from 'design-system/lib'; import { isEmpty, isUndefined } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import isEmail from 'validator/lib/isEmail'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; @@ -28,23 +35,35 @@ type InputType = 'email' | 'number' | 'password' | 'select' | 'text'; interface Props { children?: (props: { onChange: (value: string) => void }) => React.ReactNode; - description: string; + description: React.ReactNode; + hasValue?: boolean; id: string; name: string; - onChange: (value: string) => void; + onChange: (value: string | undefined) => void; options?: string[]; required?: boolean; + requiresRevaluation?: boolean; type?: InputType; value: string | undefined; } export function EmailNotificationFormField(props: Readonly) { - const { description, id, name, options, required, type = 'text', value } = props; + const { + description, + hasValue, + id, + name, + options, + required, + requiresRevaluation, + type = 'text', + value, + } = props; const [validationMessage, setValidationMessage] = React.useState(); - const handleCheck = (changedValue?: string) => { - if (isEmpty(changedValue) && required) { + const handleCheck = (changedValue: string | undefined) => { + if (changedValue !== undefined && isEmpty(changedValue) && required) { setValidationMessage(translate('settings.state.value_cant_be_empty_no_default')); return false; } @@ -58,7 +77,7 @@ export function EmailNotificationFormField(props: Readonly) { return true; }; - const onChange = (newValue: string) => { + const onChange = (newValue: string | undefined) => { handleCheck(newValue); props.onChange(newValue); }; @@ -74,25 +93,17 @@ export function EmailNotificationFormField(props: Readonly) { requiredAriaLabel={translate('field_required')} >
- {type === 'select' ? ( - - ) : ( - - )} + {hasValidationMessage && ( ) { ); } +function EmailInput( + props: Readonly<{ + hasValue: boolean | undefined; + id: string; + name: string; + onChange: (value: string | undefined) => void; + options: string[]; + required?: boolean; + requiresRevaluation?: boolean; + type: InputType; + value: string | undefined; + }>, +) { + const { type } = props; + switch (type) { + case 'password': + return ; + case 'select': + return ; + default: + return ; + } +} + function BasicInput( props: Readonly<{ + hasValue: boolean | undefined; id: string; name: string; onChange: (value: string) => void; @@ -119,7 +155,7 @@ function BasicInput( value: string | undefined; }>, ) { - const { id, onChange, name, required, type, value } = props; + const { hasValue, id, onChange, name, required, type, value } = props; return ( onChange(event.target.value)} + placeholder={ + type === 'password' && hasValue ? translate('email_notification.form.private') : undefined + } required={required} size="large" step={type === 'number' ? 1 : undefined} @@ -136,6 +175,61 @@ function BasicInput( ); } +function PasswordInput( + props: Readonly<{ + hasValue: boolean | undefined; + id: string; + name: string; + onChange: (value: string | undefined) => void; + required?: boolean; + requiresRevaluation?: boolean; + value: string | undefined; + }>, +) { + const { hasValue, id, onChange, name, required, requiresRevaluation, value } = props; + const [isEditing, setIsEditing] = React.useState(requiresRevaluation === true); + + useEffect(() => { + if (!requiresRevaluation) { + setIsEditing(!hasValue); + } + }, [hasValue, requiresRevaluation]); + + return ( +
+ onChange(event.target.value)} + required={isEditing && required} + size="large" + type="password" + value={ + hasValue && !isEditing && !requiresRevaluation + ? translate('email_notification.form.private') + : value ?? '' + } + /> + {!requiresRevaluation && ( + { + if (isEditing) { + onChange(undefined); + } + setIsEditing(!isEditing); + }} + variety={ButtonVariety.Default} + /> + )} +
+ ); +} + function SelectInput( props: Readonly<{ id: string; diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationOverview.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationOverview.tsx new file mode 100644 index 00000000000..007520342e7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationOverview.tsx @@ -0,0 +1,117 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { Button, ButtonVariety } from '@sonarsource/echoes-react'; +import { BasicSeparator, CodeSnippet } from 'design-system/lib'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { translate } from '../../../../helpers/l10n'; +import { AuthMethod, EmailConfiguration } from '../../../../types/system'; + +interface EmailTestModalProps { + emailConfiguration: EmailConfiguration; + onEditClicked: () => void; +} + +export default function EmailNotificationOverview(props: Readonly) { + const { emailConfiguration, onEditClicked } = props; + + return ( + <> + +
+
+ + {translate('email_notification.overview.heading')} + + + + + + {emailConfiguration.authMethod === AuthMethod.Basic ? ( + + ) : ( + <> + + + + + + )} + + + + + + + +
+ +
+ + ); +} + +function PublicValue({ messageKey, value }: Readonly<{ messageKey: string; value: string }>) { + return ( + <> + +
+ +
+ + ); +} + +function PrivateValue({ messageKey }: Readonly<{ messageKey: string }>) { + return ( + <> + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/EmailNotification-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/EmailNotification-it.tsx index 28f49b63b67..b481742cbf8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/EmailNotification-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/EmailNotification-it.tsx @@ -21,7 +21,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addGlobalSuccessMessage } from 'design-system/lib'; import React from 'react'; -import { byLabelText, byRole, byText } from '~sonar-aligned/helpers/testSelector'; +import { byLabelText, byRole, byTestId, byText } from '~sonar-aligned/helpers/testSelector'; import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock'; import * as api from '../../../../../api/system'; import { mockEmailConfiguration } from '../../../../../helpers/mocks/system'; @@ -52,7 +52,7 @@ const ui = { name: 'email_notification.form.basic_auth.title email_notification.form.basic_auth.description', }), selectorOAuthAuth: byRole('radio', { - name: 'email_notification.form.oauth_auth.title email_notification.form.oauth_auth.description recommended email_notification.form.oauth_auth.recommended_reason', + name: 'email_notification.form.oauth_auth.title email_notification.form.oauth_auth.description email_notification.form.oauth_auth.supported recommended email_notification.form.oauth_auth.recommended_reason', }), host: byRole('textbox', { name: 'email_notification.form.host field_required', @@ -84,12 +84,34 @@ const ui = { name: 'email_notification.form.oauth_authentication_host field_required', }), oauth_client_id: byLabelText('email_notification.form.oauth_client_id*'), + oauth_client_id_edit: byTestId('email_notification.form.oauth_client_id-edit'), + oauth_client_id_reset: byTestId('email_notification.form.oauth_client_id-reset'), oauth_client_secret: byLabelText('email_notification.form.oauth_client_secret*'), oauth_tenant: byRole('textbox', { name: 'email_notification.form.oauth_tenant field_required' }), save: byRole('button', { name: 'email_notification.form.save_configuration', }), + + // overview values + overviewHeading: byText('email_notification.overview.heading'), + overview_auth_mode: byTestId('email_notification.overview.authentication_type.value'), + overview_username: byTestId('email_notification.form.username.value'), + overview_basic_password: byTestId('email_notification.form.basic_password.value'), + overview_oauth_auth_host: byTestId('email_notification.form.oauth_authentication_host.value'), + overview_oauth_client_id: byTestId('email_notification.form.oauth_client_id.value'), + overview_oauth_client_secret: byTestId('email_notification.form.oauth_client_secret.value'), + overview_oauth_tenant: byTestId('email_notification.form.oauth_tenant.value'), + overview_host: byTestId('email_notification.form.host.value'), + overview_port: byTestId('email_notification.form.port.value'), + overview_security_protocol: byTestId('email_notification.form.security_protocol.value'), + overview_from_address: byTestId('email_notification.form.from_address.value'), + overview_from_name: byTestId('email_notification.form.from_name.value'), + overview_subject_prefix: byTestId('email_notification.form.subject_prefix.value'), + + edit: byRole('button', { + name: 'edit', + }), }; describe('Email Basic Configuration', () => { @@ -153,6 +175,41 @@ describe('Email Basic Configuration', () => { expect(addGlobalSuccessMessage).toHaveBeenCalledWith( 'email_notification.form.save_configuration.create_success', ); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.Basic); + expect(ui.overview_username.get()).toHaveTextContent('username'); + expect(ui.overview_basic_password.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_host.get()).toHaveTextContent('host'); + expect(ui.overview_port.get()).toHaveTextContent('1234'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('SSLTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('admin@localhost.com'); + expect(ui.overview_from_name.get()).toHaveTextContent('fromName'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('prefix'); + }); + + it('renders the overview after loading configuration', async () => { + systemHandler.addEmailConfiguration( + mockEmailConfiguration(AuthMethod.Basic, { id: 'email-1' }), + ); + + renderEmailNotifications(); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.Basic); + expect(ui.overview_username.get()).toHaveTextContent('username'); + expect(ui.overview_basic_password.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_host.get()).toHaveTextContent('host'); + expect(ui.overview_port.get()).toHaveTextContent('port'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('SSLTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('from_address'); + expect(ui.overview_from_name.get()).toHaveTextContent('from_name'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('subject_prefix'); }); it('can edit an existing configuration', async () => { @@ -162,6 +219,11 @@ describe('Email Basic Configuration', () => { jest.spyOn(api, 'patchEmailConfiguration'); const user = userEvent.setup(); renderEmailNotifications(); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + + await user.click(ui.edit.get()); + expect(await ui.editSubheading1.find()).toBeInTheDocument(); expect(ui.save.get()).toBeDisabled(); @@ -197,6 +259,20 @@ describe('Email Basic Configuration', () => { expect(addGlobalSuccessMessage).toHaveBeenCalledWith( 'email_notification.form.save_configuration.update_success', ); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.Basic); + expect(ui.overview_username.get()).toHaveTextContent('username-updated'); + expect(ui.overview_basic_password.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_host.get()).toHaveTextContent('host-updated'); + expect(ui.overview_port.get()).toHaveTextContent('5678'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('STARTTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('updated@email.com'); + expect(ui.overview_from_name.get()).toHaveTextContent('from_name-updated'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('subject_prefix-updated'); }); }); @@ -274,20 +350,70 @@ describe('Email Oauth Configuration', () => { expect(addGlobalSuccessMessage).toHaveBeenCalledWith( 'email_notification.form.save_configuration.create_success', ); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.OAuth); + expect(ui.overview_oauth_auth_host.get()).toHaveTextContent('oauth_auth_host'); + expect(ui.overview_oauth_client_id.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_client_secret.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_tenant.get()).toHaveTextContent('oauth_tenant'); + expect(ui.overview_host.get()).toHaveTextContent('host'); + expect(ui.overview_port.get()).toHaveTextContent('1234'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('SSLTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('admin@localhost.com'); + expect(ui.overview_from_name.get()).toHaveTextContent('fromName'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('prefix'); + expect(ui.overview_username.get()).toHaveTextContent('username'); + }); + + it('renders the overview after loading configuration', async () => { + systemHandler.addEmailConfiguration( + mockEmailConfiguration(AuthMethod.OAuth, { id: 'email-2' }), + ); + + renderEmailNotifications(); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.OAuth); + expect(ui.overview_oauth_auth_host.get()).toHaveTextContent('oauth_auth_host'); + expect(ui.overview_oauth_client_id.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_client_secret.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_tenant.get()).toHaveTextContent('oauth_tenant'); + expect(ui.overview_host.get()).toHaveTextContent('host'); + expect(ui.overview_port.get()).toHaveTextContent('port'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('SSLTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('from_address'); + expect(ui.overview_from_name.get()).toHaveTextContent('from_name'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('subject_prefix'); }); - it('can edit the oauth configuration', async () => { + it('can edit the configuration', async () => { systemHandler.addEmailConfiguration( mockEmailConfiguration(AuthMethod.OAuth, { id: 'email-1' }), ); jest.spyOn(api, 'patchEmailConfiguration'); const user = userEvent.setup(); renderEmailNotifications(); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + await user.click(ui.edit.get()); + expect(await ui.editSubheading1.find()).toBeInTheDocument(); await user.click(ui.selectorOAuthAuth.get()); expect(ui.save.get()).toBeDisabled(); + await user.type(ui.oauth_auth_host.get(), '-updated'); + await user.click(ui.oauth_client_id_edit.get()); await user.type(ui.oauth_client_id.get(), 'updated_id'); await user.type(ui.oauth_client_secret.get(), 'updated_secret'); await user.type(ui.oauth_tenant.get(), '-updated'); @@ -326,6 +452,25 @@ describe('Email Oauth Configuration', () => { expect(addGlobalSuccessMessage).toHaveBeenCalledWith( 'email_notification.form.save_configuration.update_success', ); + + expect(await ui.overviewHeading.find()).toBeInTheDocument(); + + expect(ui.overview_auth_mode.get()).toHaveTextContent(AuthMethod.OAuth); + expect(ui.overview_oauth_auth_host.get()).toHaveTextContent('oauth_auth_host'); + expect(ui.overview_oauth_client_id.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_client_secret.get()).toHaveTextContent( + 'email_notification.overview.private', + ); + expect(ui.overview_oauth_tenant.get()).toHaveTextContent('oauth_tenant'); + expect(ui.overview_host.get()).toHaveTextContent('host'); + expect(ui.overview_port.get()).toHaveTextContent('5678'); + expect(ui.overview_security_protocol.get()).toHaveTextContent('STARTTLS'); + expect(ui.overview_from_address.get()).toHaveTextContent('updated@email.com'); + expect(ui.overview_from_name.get()).toHaveTextContent('from_name-updated'); + expect(ui.overview_subject_prefix.get()).toHaveTextContent('subject_prefix-updated'); + expect(ui.overview_username.get()).toHaveTextContent('username-updated'); }); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/utils-test.ts new file mode 100644 index 00000000000..ac86bf7224c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/utils-test.ts @@ -0,0 +1,648 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { mockEmailConfiguration } from '../../../../../helpers/mocks/system'; +import { AuthMethod, EmailConfiguration } from '../../../../../types/system'; +import { checkEmailConfigurationHasChanges } from '../utils'; + +const validBasicValues = { basicPassword: 'password' }; +const validOAuthValues = { oauthClientId: 'oauthClientId', oauthClientSecret: 'oauthClientSecret' }; + +describe('checkEmailConfigurationValidity empty configurations', () => { + it('should return false if configuration is undefined', () => { + expect(checkEmailConfigurationHasChanges(null, null)).toBe(false); + }); + + it('should return true if configuration is valid and no existing configuration', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, validBasicValues), + null, + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity common props', () => { + it('should return false if authMethod is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + authMethod: undefined, + }), + null, + ), + ).toBe(false); + }); + + it('should return true if authMethod is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + authMethod: AuthMethod.Basic, + }), + null, + ), + ).toBe(true); + }); + + it('should return false if username is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + username: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, username: '' }), + null, + ), + ).toBe(false); + }); + + it('should return true if username is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, username: 'username' }), + null, + ), + ).toBe(true); + }); + + it('should return false if host is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + host: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, host: '' }), + null, + ), + ).toBe(false); + }); + + it('should return true if host is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, host: 'username' }), + null, + ), + ).toBe(true); + }); + + it('should return false if port is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + port: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, port: '' }), + null, + ), + ).toBe(false); + }); + + it('should return true if port is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, port: 'port' }), + null, + ), + ).toBe(true); + }); + + it('should return false if securityProtocol is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + securityProtocol: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + securityProtocol: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if securityProtocol is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + securityProtocol: 'securityProtocol', + }), + null, + ), + ).toBe(true); + }); + + it('should return false if fromAddress is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + fromAddress: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + fromAddress: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if fromAddress is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + fromAddress: 'fromAddress', + }), + null, + ), + ).toBe(true); + }); + + it('should return false if fromName is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + fromName: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + fromName: '', + }) as EmailConfiguration, + null, + ), + ).toBe(false); + }); + + it('should return true if fromName is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, fromName: 'fromName' }), + null, + ), + ).toBe(true); + }); + + it('should return false if subjectPrefix is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + subjectPrefix: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + subjectPrefix: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if subjectPrefix is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + ...validBasicValues, + subjectPrefix: 'subjectPrefix', + }), + null, + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity basic-auth props', () => { + it('should return false if basicPassword is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + basicPassword: undefined, + isBasicPasswordSet: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + basicPassword: '', + isBasicPasswordSet: undefined, + }), + null, + ), + ).toBe(false); + }); + + it('should return true if basicPassword is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { + basicPassword: 'basicPassword', + isBasicPasswordSet: undefined, + }), + null, + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity editing basic-auth props', () => { + it('should return false if basicPassword is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { basicPassword: undefined }), + mockEmailConfiguration(AuthMethod.Basic, validBasicValues), + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { basicPassword: '' }), + mockEmailConfiguration(AuthMethod.Basic, validBasicValues), + ), + ).toBe(false); + }); + + it('should return true if basicPassword is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { basicPassword: 'basicPassword' }), + mockEmailConfiguration(AuthMethod.Basic, validBasicValues), + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity oauth-auth props', () => { + it('should return false if oauthAuthenticationHost is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if oauthAuthenticationHost is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: 'oauthAuthenticationHost', + }), + null, + ), + ).toBe(true); + }); + + it('should return false if oauthClientId is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientId: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientId: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if oauthClientId is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientId: 'oauthClientId', + }), + null, + ), + ).toBe(true); + }); + + it('should return false if oauthClientSecret is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientSecret: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientSecret: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if oauthClientSecret is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientSecret: 'oauthClientSecret', + }), + null, + ), + ).toBe(true); + }); + + it('should return false if oauthTenant is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: undefined, + }), + null, + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: '', + }), + null, + ), + ).toBe(false); + }); + + it('should return true if oauthTenant is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: 'oauthTenant', + }), + null, + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity editing oauth-auth props', () => { + it('should return false if oauthAuthenticationHost is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: undefined, + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: '', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(false); + }); + + it('should return true if oauthAuthenticationHost is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthAuthenticationHost: 'oauthAuthenticationHost', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(true); + }); + + it('should return true if oauthClientId is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientId: 'oauthClientId', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(true); + }); + + it('should return true if oauthClientSecret is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthClientSecret: 'oauthClientSecret', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(true); + }); + + it('should return false if oauthTenant is not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: undefined, + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(false); + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: '', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(false); + }); + + it('should return true if oauthTenant is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + ...validOAuthValues, + oauthTenant: 'oauthTenant', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(true); + }); + + it('should return false if oauthClientId is edited in isolation', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + oauthClientId: 'oauthClientId', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(false); + }); + + it('should return true if oauthClientSecret is edited in isolation', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + oauthClientSecret: 'oauthClientSecret', + }), + mockEmailConfiguration(AuthMethod.OAuth), + ), + ).toBe(true); + }); +}); + +describe('checkEmailConfigurationValidity editing', () => { + it('should return false if configuration has not changed', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic), + mockEmailConfiguration(AuthMethod.Basic), + ), + ).toBe(false); + }); + + it('should return true if configuration has changed', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, { ...validBasicValues, username: 'new-username' }), + mockEmailConfiguration(AuthMethod.Basic), + ), + ).toBe(true); + }); + + it('should return false if configuration type has changed and password not set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { + isOauthClientIdSet: false, + isOauthClientSecretSet: false, + }), + mockEmailConfiguration(AuthMethod.Basic), + ), + ).toBe(false); + }); + + it('should return true if configuration type has changed and password is set', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, validOAuthValues), + mockEmailConfiguration(AuthMethod.Basic), + ), + ).toBe(true); + }); + + it('should return true if configuration type has changed and password already exists', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.Basic, validBasicValues), + mockEmailConfiguration(AuthMethod.OAuth, { isBasicPasswordSet: true }), + ), + ).toBe(true); + }); + + it('should return true if configuration type has changed and oauthClientId & oauthClientSecret already exist', () => { + expect( + checkEmailConfigurationHasChanges( + mockEmailConfiguration(AuthMethod.OAuth, { oauthClientSecret: 'oauthClientSecret' }), + mockEmailConfiguration(AuthMethod.Basic, { + isOauthClientIdSet: true, + isOauthClientSecretSet: true, + }), + ), + ).toBe(true); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/utils.ts b/server/sonar-web/src/main/js/apps/settings/components/email-notification/utils.ts index 4db59fac05f..ab13a9a2380 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/utils.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/utils.ts @@ -17,8 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isUndefined } from 'lodash'; -import { AuthMethod, EmailConfiguration } from '../../../../types/system'; +import { + AuthMethod, + EmailConfiguration, + EmailConfigurationBasicAuth, + EmailConfigurationOAuth, +} from '../../../../types/system'; export const AUTH_METHOD = 'auth-method'; @@ -48,36 +52,57 @@ export interface EmailNotificationGroupProps { onChange: (newValue: Partial) => void; } -export function checkEmailConfigurationValidity(configuration: EmailConfiguration): boolean { +const COMMON_EMAIL_PROPS: (keyof EmailConfiguration)[] = [ + 'authMethod', + 'username', + 'host', + 'port', + 'securityProtocol', + 'fromAddress', + 'fromName', + 'subjectPrefix', +]; + +export function checkEmailConfigurationHasChanges( + configuration: EmailConfiguration | null, + originalConfiguration: EmailConfiguration | null, +): boolean { + if (!configuration) { + return false; + } + + const isEditing = originalConfiguration !== null; + const isOAuth = configuration.authMethod === AuthMethod.OAuth; + let isValid = false; - const commonProps: (keyof EmailConfiguration)[] = [ - 'authMethod', - 'username', - 'host', - 'port', - 'securityProtocol', - 'fromAddress', - 'fromName', - 'subjectPrefix', - ]; - if (configuration.authMethod === AuthMethod.Basic) { - isValid = checkRequiredPropsAreValid(configuration, [...commonProps, 'basicPassword']); - } else { - isValid = checkRequiredPropsAreValid(configuration, [ - ...commonProps, + if (isOAuth) { + const oauthClientIdChanged = + configuration.isOauthClientIdSet === true && + checkRequiredPropsAreValid(configuration as EmailConfigurationOAuth, ['oauthClientId']); + + const privatePropsToCheck: (keyof EmailConfigurationOAuth)[] = ['oauthClientSecret']; + if (!isEditing || oauthClientIdChanged || !configuration.isOauthClientIdSet) { + privatePropsToCheck.push('oauthClientId'); + } + + isValid = checkRequiredPropsAreValid(configuration as EmailConfigurationOAuth, [ + ...COMMON_EMAIL_PROPS, 'oauthAuthenticationHost', - 'oauthClientId', - 'oauthClientSecret', 'oauthTenant', + ...privatePropsToCheck, + ]); + } else { + isValid = checkRequiredPropsAreValid(configuration as EmailConfigurationBasicAuth, [ + ...COMMON_EMAIL_PROPS, + 'basicPassword', ]); } return isValid; } +// Check if required props are present and contain a value that is not an empty string. function checkRequiredPropsAreValid(obj: T, props: (keyof T)[]): boolean { - return props.every( - (prop) => !isUndefined(obj[prop]) && typeof obj[prop] === 'string' && obj[prop].length > 0, - ); + return props.every((prop) => typeof obj[prop] === 'string' && obj[prop]); } diff --git a/server/sonar-web/src/main/js/types/system.ts b/server/sonar-web/src/main/js/types/system.ts index 137d8fa3a83..818297fbfaf 100644 --- a/server/sonar-web/src/main/js/types/system.ts +++ b/server/sonar-web/src/main/js/types/system.ts @@ -59,23 +59,10 @@ export enum AuthMethod { OAuth = 'OAUTH', } -export type EmailConfiguration = ( - | { - authMethod: AuthMethod.Basic; - basicPassword: string; - readonly isBasicPasswordSet?: boolean; - } - | { - authMethod: AuthMethod.OAuth; - readonly isOauthClientIdSet?: boolean; - readonly isOauthClientSecretSet?: boolean; - oauthAuthenticationHost: string; - oauthClientId: string; - oauthClientSecret: string; - oauthTenant: string; - } -) & - EmailConfigurationCommon; +export type EmailConfiguration = EmailConfigurationAuth & EmailConfigurationCommon; +export type EmailConfigurationAuth = EmailNotificationBasicAuth | EmailNotificationOAuth; +export type EmailConfigurationBasicAuth = EmailNotificationBasicAuth & EmailConfigurationCommon; +export type EmailConfigurationOAuth = EmailNotificationOAuth & EmailConfigurationCommon; interface EmailConfigurationCommon { fromAddress: string; @@ -87,3 +74,19 @@ interface EmailConfigurationCommon { subjectPrefix: string; username: string; } + +interface EmailNotificationBasicAuth { + authMethod: AuthMethod.Basic; + basicPassword: string; + readonly isBasicPasswordSet?: boolean; +} + +interface EmailNotificationOAuth { + authMethod: AuthMethod.OAuth; + readonly isOauthClientIdSet?: boolean; + readonly isOauthClientSecretSet?: boolean; + oauthAuthenticationHost: string; + oauthClientId: string; + oauthClientSecret: string; + oauthTenant: string; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f039cd6df6e..82ce90dac17 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2696,7 +2696,8 @@ email_notification.form.username.description=Username used to authenticate to th email_notification.form.basic_password=SMTP password email_notification.form.basic_password.description=Password used to authenticate to the SMTP server. email_notification.form.oauth_auth.title=Modern Authentication -email_notification.form.oauth_auth.description=Authenticate with OAuth Microsoft +email_notification.form.oauth_auth.description=Authenticate with OAuth +email_notification.form.oauth_auth.supported=Supported: Microsoft email_notification.form.oauth_auth.recommended_reason=for stronger security compliance email_notification.form.oauth_authentication_host=Authentication host email_notification.form.oauth_authentication_host.description=Host of the Identity Provider issuing access tokens. @@ -2723,6 +2724,11 @@ email_notification.form.save_configuration.create_success=Email configuration sa email_notification.form.save_configuration.update_success=Email configuration updated successfully. email_notification.form.delete_configuration=Delete configuration email_notification.state.value_should_be_valid_email=A valid email address is required. +email_notification.overview.heading=SMTP configuration settings +email_notification.overview.authentication_type=Authentication type +email_notification.overview.private=Hidden for security reasons +email_notification.form.private=************** +email_notification.overview.value={0} value email_configuration.test.title=Test Configuration email_configuration.test.to_address=To -- 2.39.5