/* | |||||
* 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); | |||||
}; | |||||
} |
import { ExtendedSettingDefinition } from '../../../../types/settings'; | import { ExtendedSettingDefinition } from '../../../../types/settings'; | ||||
import { AUTHENTICATION_CATEGORY } from '../../constants'; | import { AUTHENTICATION_CATEGORY } from '../../constants'; | ||||
import CategoryDefinitionsList from '../CategoryDefinitionsList'; | import CategoryDefinitionsList from '../CategoryDefinitionsList'; | ||||
import SamlAuthentication from './SamlAuthentication'; | |||||
interface Props { | interface Props { | ||||
definitions: ExtendedSettingDefinition[]; | definitions: ExtendedSettingDefinition[]; | ||||
role="tabpanel" | role="tabpanel" | ||||
aria-labelledby={getTabId(currentTab)} | aria-labelledby={getTabId(currentTab)} | ||||
id={getTabPanelId(currentTab)}> | id={getTabPanelId(currentTab)}> | ||||
<div className="big-padded"> | |||||
<div className="big-padded-top big-padded-left big-padded-right"> | |||||
<Alert variant="info"> | <Alert variant="info"> | ||||
<FormattedMessage | <FormattedMessage | ||||
id="settings.authentication.help" | id="settings.authentication.help" | ||||
}} | }} | ||||
/> | /> | ||||
</Alert> | </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> | ||||
</div> | </div> | ||||
)} | )} |
/* | |||||
* 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; |
/* | |||||
* 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> | |||||
); | |||||
} |
/* | |||||
* 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> | |||||
)} | |||||
</> | |||||
); | |||||
} |
/* | |||||
* 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} | |||||
/> | |||||
); | |||||
} |
import { screen } from '@testing-library/react'; | import { screen } from '@testing-library/react'; | ||||
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import React from 'react'; | import React from 'react'; | ||||
import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock'; | |||||
import { mockDefinition } from '../../../../../helpers/mocks/settings'; | |||||
import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | ||||
import { ExtendedSettingDefinition, SettingType } from '../../../../../types/settings'; | |||||
import Authentication from '../Authentication'; | 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 () => { | it('should render tabs and allow navigation', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderAuthentication(); | |||||
renderAuthentication([]); | |||||
expect(screen.getAllByRole('tab')).toHaveLength(4); | expect(screen.getAllByRole('tab')).toHaveLength(4); | ||||
); | ); | ||||
}); | }); | ||||
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} />); | |||||
} | } |
overflow-y: auto; | overflow-y: auto; | ||||
overflow-x: hidden; | 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; | |||||
} |
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.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=If you need help setting up authentication, read our dedicated {link}. | ||||
settings.authentication.help.link=documentation | settings.authentication.help.link=documentation | ||||
settings.authentication.saml.form.save=Save configuration | |||||
settings.pr_decoration.binding.category=DevOps Platform Integration | 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. | settings.pr_decoration.binding.no_bindings=A system administrator needs to enable this feature in the global settings. |