@@ -0,0 +1,76 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash'; | |||
import { mockSettingValue } from '../../helpers/mocks/settings'; | |||
import { BranchParameters } from '../../types/branch-like'; | |||
import { SettingDefinition, SettingValue } from '../../types/settings'; | |||
import { getValues, resetSettingValue, setSettingValue } from '../settings'; | |||
export default class AuthenticationServiceMock { | |||
settingValues: SettingValue[]; | |||
defaulSettingValues: SettingValue[] = [ | |||
mockSettingValue({ key: 'test1', value: '' }), | |||
mockSettingValue({ key: 'test2', value: 'test2' }), | |||
mockSettingValue({ key: 'sonar.auth.saml.certificate.secured' }), | |||
mockSettingValue({ key: 'sonar.auth.saml.enabled', value: 'false' }) | |||
]; | |||
constructor() { | |||
this.settingValues = cloneDeep(this.defaulSettingValues); | |||
(getValues as jest.Mock).mockImplementation(this.getValuesHandler); | |||
(setSettingValue as jest.Mock).mockImplementation(this.setValueHandler); | |||
(resetSettingValue as jest.Mock).mockImplementation(this.resetValueHandler); | |||
} | |||
getValuesHandler = (data: { keys: string; component?: string } & BranchParameters) => { | |||
if (data.keys) { | |||
return Promise.resolve( | |||
this.settingValues.filter(set => data.keys.split(',').includes(set.key)) | |||
); | |||
} | |||
return Promise.resolve(this.settingValues); | |||
}; | |||
setValueHandler = (definition: SettingDefinition, value: string) => { | |||
const updatedSettingValue = this.settingValues.find(set => set.key === definition.key); | |||
if (updatedSettingValue) { | |||
updatedSettingValue.value = value; | |||
} | |||
return Promise.resolve(); | |||
}; | |||
resetValueHandler = (data: { keys: string; component?: string } & BranchParameters) => { | |||
if (data.keys) { | |||
return Promise.resolve( | |||
this.settingValues.map(set => { | |||
if (data.keys.includes(set.key)) { | |||
set.value = ''; | |||
} | |||
return set; | |||
}) | |||
); | |||
} | |||
return Promise.resolve(this.settingValues); | |||
}; | |||
resetValues = () => { | |||
this.settingValues = cloneDeep(this.defaulSettingValues); | |||
}; | |||
} |
@@ -31,6 +31,7 @@ import { AlmKeys } from '../../../../types/alm-settings'; | |||
import { ExtendedSettingDefinition } from '../../../../types/settings'; | |||
import { AUTHENTICATION_CATEGORY } from '../../constants'; | |||
import CategoryDefinitionsList from '../CategoryDefinitionsList'; | |||
import SamlAuthentication from './SamlAuthentication'; | |||
interface Props { | |||
definitions: ExtendedSettingDefinition[]; | |||
@@ -134,7 +135,7 @@ export default function Authentication(props: Props) { | |||
role="tabpanel" | |||
aria-labelledby={getTabId(currentTab)} | |||
id={getTabPanelId(currentTab)}> | |||
<div className="big-padded"> | |||
<div className="big-padded-top big-padded-left big-padded-right"> | |||
<Alert variant="info"> | |||
<FormattedMessage | |||
id="settings.authentication.help" | |||
@@ -151,12 +152,20 @@ export default function Authentication(props: Props) { | |||
}} | |||
/> | |||
</Alert> | |||
<CategoryDefinitionsList | |||
category={AUTHENTICATION_CATEGORY} | |||
definitions={definitions} | |||
subCategory={currentTab} | |||
displaySubCategoryTitle={false} | |||
/> | |||
{currentTab === SAML && ( | |||
<SamlAuthentication | |||
definitions={definitions.filter(def => def.subCategory === SAML)} | |||
/> | |||
)} | |||
{currentTab !== SAML && ( | |||
<CategoryDefinitionsList | |||
category={AUTHENTICATION_CATEGORY} | |||
definitions={definitions} | |||
subCategory={currentTab} | |||
displaySubCategoryTitle={false} | |||
/> | |||
)} | |||
</div> | |||
</div> | |||
)} |
@@ -0,0 +1,266 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { keyBy } from 'lodash'; | |||
import React from 'react'; | |||
import { getValues, resetSettingValue, setSettingValue } from '../../../../api/settings'; | |||
import { SubmitButton } from '../../../../components/controls/buttons'; | |||
import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { parseError } from '../../../../helpers/request'; | |||
import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../../types/settings'; | |||
import SamlFormField from './SamlFormField'; | |||
import SamlToggleField from './SamlToggleField'; | |||
interface SamlAuthenticationProps { | |||
definitions: ExtendedSettingDefinition[]; | |||
} | |||
interface SamlAuthenticationState { | |||
settingValue: Pick<SettingValue, 'key' | 'value'>[]; | |||
submitting: boolean; | |||
dirtyFields: string[]; | |||
securedFieldsSubmitted: string[]; | |||
error: { [key: string]: string }; | |||
} | |||
const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled'; | |||
const OPTIONAL_FIELDS = [ | |||
'sonar.auth.saml.sp.certificate.secured', | |||
'sonar.auth.saml.sp.privateKey.secured', | |||
'sonar.auth.saml.signature.enabled', | |||
'sonar.auth.saml.user.email', | |||
'sonar.auth.saml.group.name' | |||
]; | |||
class SamlAuthentication extends React.PureComponent< | |||
SamlAuthenticationProps, | |||
SamlAuthenticationState | |||
> { | |||
constructor(props: SamlAuthenticationProps) { | |||
super(props); | |||
const settingValue = props.definitions.map(def => { | |||
return { | |||
key: def.key | |||
}; | |||
}); | |||
this.state = { | |||
settingValue, | |||
submitting: false, | |||
dirtyFields: [], | |||
securedFieldsSubmitted: [], | |||
error: {} | |||
}; | |||
} | |||
componentDidMount() { | |||
const { definitions } = this.props; | |||
const keys = definitions.map(definition => definition.key).join(','); | |||
this.loadSettingValues(keys); | |||
} | |||
onFieldChange = (id: string, value: string | boolean) => { | |||
const { settingValue, dirtyFields } = this.state; | |||
const updatedSettingValue = settingValue?.map(set => { | |||
if (set.key === id) { | |||
set.value = String(value); | |||
} | |||
return set; | |||
}); | |||
if (!dirtyFields.includes(id)) { | |||
const updatedDirtyFields = [...dirtyFields, id]; | |||
this.setState({ | |||
dirtyFields: updatedDirtyFields | |||
}); | |||
} | |||
this.setState({ | |||
settingValue: updatedSettingValue | |||
}); | |||
}; | |||
async loadSettingValues(keys: string) { | |||
const { settingValue, securedFieldsSubmitted } = this.state; | |||
const values = await getValues({ | |||
keys | |||
}); | |||
const valuesByDefinitionKey = keyBy(values, 'key'); | |||
const updatedSecuredFieldsSubmitted: string[] = [...securedFieldsSubmitted]; | |||
const updateSettingValue = settingValue?.map(set => { | |||
if (valuesByDefinitionKey[set.key]) { | |||
set.value = | |||
valuesByDefinitionKey[set.key].value ?? valuesByDefinitionKey[set.key].parentValue; | |||
} | |||
if ( | |||
this.isSecuredField(set.key) && | |||
valuesByDefinitionKey[set.key] && | |||
!securedFieldsSubmitted.includes(set.key) | |||
) { | |||
updatedSecuredFieldsSubmitted.push(set.key); | |||
} | |||
return set; | |||
}); | |||
this.setState({ | |||
settingValue: updateSettingValue, | |||
securedFieldsSubmitted: updatedSecuredFieldsSubmitted | |||
}); | |||
} | |||
isSecuredField = (key: string) => { | |||
const { definitions } = this.props; | |||
const fieldDefinition = definitions.find(def => def.key === key); | |||
if (fieldDefinition && fieldDefinition.type === SettingType.PASSWORD) { | |||
return true; | |||
} | |||
return false; | |||
}; | |||
onSaveConfig = async () => { | |||
const { settingValue, dirtyFields } = this.state; | |||
const { definitions } = this.props; | |||
if (dirtyFields.length === 0) { | |||
return; | |||
} | |||
this.setState({ submitting: true, error: {} }); | |||
const promises: Promise<void>[] = []; | |||
settingValue?.forEach(set => { | |||
const definition = definitions.find(def => def.key === set.key); | |||
if (definition && set.value !== undefined && dirtyFields.includes(set.key)) { | |||
const apiCall = | |||
set.value.length > 0 | |||
? setSettingValue(definition, set.value) | |||
: resetSettingValue({ keys: definition.key }); | |||
const promise = apiCall.catch(async e => { | |||
const { error } = this.state; | |||
const validationMessage = await parseError(e as Response); | |||
this.setState({ | |||
submitting: false, | |||
dirtyFields: [], | |||
error: { ...error, ...{ [set.key]: validationMessage } } | |||
}); | |||
}); | |||
promises.push(promise); | |||
} | |||
}); | |||
await Promise.all(promises); | |||
await this.loadSettingValues(dirtyFields.join(',')); | |||
this.setState({ submitting: false, dirtyFields: [] }); | |||
}; | |||
allowEnabling = () => { | |||
const { settingValue, securedFieldsSubmitted } = this.state; | |||
const enabledFlagSettingValue = settingValue.find(set => set.key === SAML_ENABLED_FIELD); | |||
if (enabledFlagSettingValue && enabledFlagSettingValue.value === 'true') { | |||
return true; | |||
} | |||
for (const setting of settingValue) { | |||
const isMandatory = !OPTIONAL_FIELDS.includes(setting.key); | |||
const isSecured = this.isSecuredField(setting.key); | |||
const isSecuredAndNotSubmitted = isSecured && !securedFieldsSubmitted.includes(setting.key); | |||
const isNotSecuredAndNotSubmitted = | |||
!isSecured && (setting.value === '' || setting.value === undefined); | |||
if (isMandatory && (isSecuredAndNotSubmitted || isNotSecuredAndNotSubmitted)) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
}; | |||
onEnableFlagChange = (value: boolean) => { | |||
const { settingValue, dirtyFields } = this.state; | |||
const updatedSettingValue = settingValue?.map(set => { | |||
if (set.key === SAML_ENABLED_FIELD) { | |||
set.value = String(value); | |||
} | |||
return set; | |||
}); | |||
this.setState( | |||
{ | |||
settingValue: updatedSettingValue, | |||
dirtyFields: [...dirtyFields, SAML_ENABLED_FIELD] | |||
}, | |||
() => { | |||
this.onSaveConfig(); | |||
} | |||
); | |||
}; | |||
render() { | |||
const { definitions } = this.props; | |||
const { submitting, settingValue, securedFieldsSubmitted, error, dirtyFields } = this.state; | |||
const enabledFlagDefinition = definitions.find(def => def.key === SAML_ENABLED_FIELD); | |||
return ( | |||
<div> | |||
{definitions.map(def => { | |||
if (def.key === SAML_ENABLED_FIELD) { | |||
return null; | |||
} | |||
return ( | |||
<SamlFormField | |||
settingValue={settingValue?.find(set => set.key === def.key)} | |||
definition={def} | |||
mandatory={!OPTIONAL_FIELDS.includes(def.key)} | |||
onFieldChange={this.onFieldChange} | |||
showSecuredTextArea={ | |||
!securedFieldsSubmitted.includes(def.key) || dirtyFields.includes(def.key) | |||
} | |||
error={error} | |||
key={def.key} | |||
/> | |||
); | |||
})} | |||
<div className="fixed-footer padded-left padded-right"> | |||
{enabledFlagDefinition && ( | |||
<div> | |||
<label className="h3 spacer-right">{enabledFlagDefinition.name}</label> | |||
<SamlToggleField | |||
definition={enabledFlagDefinition} | |||
settingValue={settingValue?.find(set => set.key === enabledFlagDefinition.key)} | |||
toggleDisabled={!this.allowEnabling()} | |||
onChange={this.onEnableFlagChange} | |||
/> | |||
</div> | |||
)} | |||
<div> | |||
<SubmitButton onClick={this.onSaveConfig}> | |||
{translate('settings.authentication.saml.form.save')} | |||
<DeferredSpinner className="spacer-left" loading={submitting} /> | |||
</SubmitButton> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
export default SamlAuthentication; |
@@ -0,0 +1,93 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 React from 'react'; | |||
import ValidationInput, { | |||
ValidationInputErrorPlacement | |||
} from '../../../../components/controls/ValidationInput'; | |||
import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker'; | |||
import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../../types/settings'; | |||
import SamlSecuredField from './SamlSecuredField'; | |||
import SamlToggleField from './SamlToggleField'; | |||
interface SamlToggleFieldProps { | |||
settingValue?: SettingValue; | |||
definition: ExtendedSettingDefinition; | |||
mandatory?: boolean; | |||
onFieldChange: (key: string, value: string | boolean) => void; | |||
showSecuredTextArea?: boolean; | |||
error: { [key: string]: string }; | |||
} | |||
const SAML_SIGNATURE_FIELD = 'sonar.auth.saml.signature.enabled'; | |||
export default function SamlFormField(props: SamlToggleFieldProps) { | |||
const { mandatory = false, definition, settingValue, showSecuredTextArea = true, error } = props; | |||
return ( | |||
<div className="settings-definition" key={definition.key}> | |||
<div className="settings-definition-left"> | |||
<label className="h3" htmlFor={definition.key}> | |||
{definition.name} | |||
</label> | |||
{mandatory && <MandatoryFieldMarker />} | |||
{definition.description && ( | |||
<div className="markdown small spacer-top">{definition.description}</div> | |||
)} | |||
</div> | |||
<div className="settings-definition-right big-padded-top display-flex-column"> | |||
{definition.type === SettingType.PASSWORD && ( | |||
<SamlSecuredField | |||
definition={definition} | |||
settingValue={settingValue} | |||
onFieldChange={props.onFieldChange} | |||
showTextArea={showSecuredTextArea} | |||
/> | |||
)} | |||
{definition.type === SettingType.BOOLEAN && ( | |||
<SamlToggleField | |||
definition={definition} | |||
settingValue={settingValue} | |||
toggleDisabled={false} | |||
onChange={val => props.onFieldChange(SAML_SIGNATURE_FIELD, val)} | |||
/> | |||
)} | |||
{definition.type === undefined && ( | |||
<ValidationInput | |||
error={error[definition.key]} | |||
errorPlacement={ValidationInputErrorPlacement.Bottom} | |||
isValid={false} | |||
isInvalid={Boolean(error[definition.key])}> | |||
<input | |||
className="width-100" | |||
id={definition.key} | |||
maxLength={100} | |||
name={definition.key} | |||
onChange={e => props.onFieldChange(definition.key, e.currentTarget.value)} | |||
size={50} | |||
type="text" | |||
value={settingValue?.value ?? ''} | |||
aria-label={definition.key} | |||
/> | |||
</ValidationInput> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,67 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 React, { useEffect } from 'react'; | |||
import { ButtonLink } from '../../../../components/controls/buttons'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { ExtendedSettingDefinition, SettingValue } from '../../../../types/settings'; | |||
interface SamlToggleFieldProps { | |||
onFieldChange: (key: string, value: string) => void; | |||
settingValue?: SettingValue; | |||
definition: ExtendedSettingDefinition; | |||
optional?: boolean; | |||
showTextArea: boolean; | |||
} | |||
export default function SamlSecuredField(props: SamlToggleFieldProps) { | |||
const { settingValue, definition, optional = true, showTextArea } = props; | |||
const [showField, setShowField] = React.useState(showTextArea); | |||
useEffect(() => { | |||
setShowField(showTextArea); | |||
}, [showTextArea]); | |||
return ( | |||
<> | |||
{showField && ( | |||
<textarea | |||
className="width-100" | |||
id={definition.key} | |||
maxLength={2000} | |||
onChange={e => props.onFieldChange(definition.key, e.currentTarget.value)} | |||
required={!optional} | |||
rows={5} | |||
value={settingValue?.value ?? ''} | |||
/> | |||
)} | |||
{!showField && ( | |||
<div> | |||
<p>{translate('settings.almintegration.form.secret.field')}</p> | |||
<ButtonLink | |||
onClick={() => { | |||
setShowField(true); | |||
}}> | |||
{translate('settings.almintegration.form.secret.update_field')} | |||
</ButtonLink> | |||
</div> | |||
)} | |||
</> | |||
); | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 React from 'react'; | |||
import Toggle from '../../../../components/controls/Toggle'; | |||
import { ExtendedSettingDefinition, SettingValue } from '../../../../types/settings'; | |||
interface SamlToggleFieldProps { | |||
toggleDisabled: boolean; | |||
onChange: (value: boolean) => void; | |||
settingValue?: SettingValue; | |||
definition: ExtendedSettingDefinition; | |||
} | |||
export default function SamlToggleField(props: SamlToggleFieldProps) { | |||
const { toggleDisabled, settingValue, definition } = props; | |||
return ( | |||
<Toggle | |||
name={definition.key} | |||
onChange={props.onChange} | |||
value={settingValue?.value ?? ''} | |||
disabled={toggleDisabled} | |||
/> | |||
); | |||
} |
@@ -20,12 +20,25 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import React from 'react'; | |||
import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock'; | |||
import { mockDefinition } from '../../../../../helpers/mocks/settings'; | |||
import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | |||
import { ExtendedSettingDefinition, SettingType } from '../../../../../types/settings'; | |||
import Authentication from '../Authentication'; | |||
jest.mock('../../../../../api/settings'); | |||
let handler: AuthenticationServiceMock; | |||
beforeEach(() => { | |||
handler = new AuthenticationServiceMock(); | |||
}); | |||
afterEach(() => handler.resetValues()); | |||
it('should render tabs and allow navigation', async () => { | |||
const user = userEvent.setup(); | |||
renderAuthentication(); | |||
renderAuthentication([]); | |||
expect(screen.getAllByRole('tab')).toHaveLength(4); | |||
@@ -40,6 +53,84 @@ it('should render tabs and allow navigation', async () => { | |||
); | |||
}); | |||
function renderAuthentication() { | |||
renderComponent(<Authentication definitions={[]} />); | |||
it('should allow user to edit fields and save configuration', async () => { | |||
const user = userEvent.setup(); | |||
const definitions = [ | |||
mockDefinition({ | |||
key: 'test1', | |||
category: 'authentication', | |||
subCategory: 'saml', | |||
name: 'test1', | |||
description: 'desc1' | |||
}), | |||
mockDefinition({ | |||
key: 'test2', | |||
category: 'authentication', | |||
subCategory: 'saml', | |||
name: 'test2', | |||
description: 'desc2' | |||
}), | |||
mockDefinition({ | |||
key: 'sonar.auth.saml.certificate.secured', | |||
category: 'authentication', | |||
subCategory: 'saml', | |||
name: 'Certificate', | |||
description: 'Secured certificate', | |||
type: SettingType.PASSWORD | |||
}), | |||
mockDefinition({ | |||
key: 'sonar.auth.saml.enabled', | |||
category: 'authentication', | |||
subCategory: 'saml', | |||
name: 'Enabled', | |||
description: 'To enable the flag', | |||
type: SettingType.BOOLEAN | |||
}) | |||
]; | |||
renderAuthentication(definitions); | |||
expect(screen.getByRole('button', { name: 'off' })).toHaveAttribute('aria-disabled', 'true'); | |||
// update fields | |||
await user.click(screen.getByRole('textbox', { name: 'test1' })); | |||
await user.keyboard('new test1'); | |||
await user.click(screen.getByRole('textbox', { name: 'test2' })); | |||
await user.keyboard('new test2'); | |||
// check if enable is allowed after updating | |||
expect(screen.getByRole('button', { name: 'off' })).toHaveAttribute('aria-disabled', 'false'); | |||
// reset value | |||
await user.click(screen.getByRole('textbox', { name: 'test2' })); | |||
await user.keyboard('{Control>}a{/Control}{Backspace}'); | |||
await user.click(screen.getByRole('button', { name: 'settings.authentication.saml.form.save' })); | |||
expect(screen.getByRole('button', { name: 'off' })).toHaveAttribute('aria-disabled', 'true'); | |||
await user.click(screen.getByRole('textbox', { name: 'test2' })); | |||
await user.keyboard('new test2'); | |||
expect(screen.getByRole('button', { name: 'off' })).toHaveAttribute('aria-disabled', 'false'); | |||
expect( | |||
screen.getByRole('button', { name: 'settings.almintegration.form.secret.update_field' }) | |||
).toBeInTheDocument(); | |||
await user.click( | |||
screen.getByRole('button', { name: 'settings.almintegration.form.secret.update_field' }) | |||
); | |||
// check for secure fields | |||
expect(screen.getByRole('textbox', { name: 'Certificate' })).toBeInTheDocument(); | |||
await user.click(screen.getByRole('textbox', { name: 'Certificate' })); | |||
await user.keyboard('new certificate'); | |||
// enable the configuration | |||
await user.click(screen.getByRole('button', { name: 'off' })); | |||
expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: 'settings.authentication.saml.form.save' })); | |||
// check after switching tab that the flag is still enabled | |||
await user.click(screen.getByRole('tab', { name: 'github GitHub' })); | |||
await user.click(screen.getByRole('tab', { name: 'SAML' })); | |||
expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument(); | |||
}); | |||
function renderAuthentication(definitions: ExtendedSettingDefinition[]) { | |||
renderComponent(<Authentication definitions={definitions} />); | |||
} |
@@ -199,3 +199,15 @@ | |||
overflow-y: auto; | |||
overflow-x: hidden; | |||
} | |||
.fixed-footer { | |||
position: sticky; | |||
bottom: 0px; | |||
height: 50px; | |||
align-items: center; | |||
display: flex; | |||
border: 1px solid var(--gray80); | |||
background-color: white; | |||
justify-content: space-between; | |||
margin: 0px -16px; | |||
} |
@@ -1265,6 +1265,7 @@ settings.authentication.title=Authentication | |||
settings.authentication.description=The following settings allow you to delegate authentication via SAML, or any of the following DevOps Platforms: GitHub, GitLab, and Bitbucket. | |||
settings.authentication.help=If you need help setting up authentication, read our dedicated {link}. | |||
settings.authentication.help.link=documentation | |||
settings.authentication.saml.form.save=Save configuration | |||
settings.pr_decoration.binding.category=DevOps Platform Integration | |||
settings.pr_decoration.binding.no_bindings=A system administrator needs to enable this feature in the global settings. |