diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index 1040a17584a..043f8026784 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -369,6 +369,13 @@ describe('Datasource Management: Create Datasource form with registered Auth Typ const mockSubmitHandler = jest.fn(); const mockTestConnectionHandler = jest.fn(); const mockCancelHandler = jest.fn(); + const changeTextFieldValue = (testSubjId: string, value: string) => { + component.find(testSubjId).last().simulate('change', { + target: { + value, + }, + }); + }; test('should call registered crendential form at the first round when registered method is at the first place and username & password disabled', () => { const mockCredentialForm = jest.fn(); @@ -517,4 +524,49 @@ describe('Datasource Management: Create Datasource form with registered Auth Typ expect(mockCredentialForm).not.toHaveBeenCalled(); }); }); + + test('should create data source with registered Auth when all fields are valid', () => { + const mockCredentialForm = jest.fn(); + const authMethodToBeTested = { + name: 'Some Auth Type', + credentialSourceOption: { + value: 'Some Auth Type', + inputDisplay: 'some input', + }, + credentialForm: mockCredentialForm, + crendentialFormField: { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }, + } as AuthenticationMethod; + + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTested); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + changeTextFieldValue(titleIdentifier, 'test'); + changeTextFieldValue(descriptionIdentifier, 'test'); + changeTextFieldValue(endpointIdentifier, 'https://test.com'); + + findTestSubject(component, 'createDataSourceButton').simulate('click'); + + expect(mockSubmitHandler).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index 25b082b8c6a..ab5df7220c7 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -39,7 +39,11 @@ import { isTitleValid, performDataSourceFormValidation, } from '../../../validation'; -import { getDefaultAuthMethod, isValidUrl } from '../../../utils'; +import { + extractRegisteredAuthTypeCredentials, + getDefaultAuthMethod, + isValidUrl, +} from '../../../utils'; export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; @@ -55,8 +59,12 @@ export interface CreateDataSourceState { description: string; endpoint: string; auth: { - type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + type: AuthType | string; + credentials: + | UsernamePasswordTypedContent + | SigV4Content + | { [key: string]: string } + | undefined; }; } @@ -102,7 +110,12 @@ export class CreateDataSourceForm extends React.Component< /* Validations */ isFormValid = () => { - return performDataSourceFormValidation(this.state, this.props.existingDatasourceNamesList, ''); + return performDataSourceFormValidation( + this.state, + this.props.existingDatasourceNamesList, + '', + this.authenticationMethodRegistery + ); }; /* Events */ @@ -298,22 +311,29 @@ export class CreateDataSourceForm extends React.Component< getFormValues = (): DataSourceAttributes => { let credentials = this.state.auth.credentials; - if (this.state.auth.type === AuthType.UsernamePasswordType) { + const authType = this.state.auth.type; + + if (authType === AuthType.NoAuth) { + credentials = {}; + } else if (authType === AuthType.UsernamePasswordType) { credentials = { username: this.state.auth.credentials.username, password: this.state.auth.credentials.password, } as UsernamePasswordTypedContent; - } - if (this.state.auth.type === AuthType.SigV4) { + } else if (authType === AuthType.SigV4) { credentials = { region: this.state.auth.credentials.region, accessKey: this.state.auth.credentials.accessKey, secretKey: this.state.auth.credentials.secretKey, service: this.state.auth.credentials.service || SigV4ServiceName.OpenSearch, } as SigV4Content; - } - if (this.state.auth.type === AuthType.NoAuth) { - credentials = {}; + } else { + const currentCredentials = (credentials ?? {}) as { [key: string]: string }; + credentials = extractRegisteredAuthTypeCredentials( + currentCredentials, + authType, + this.authenticationMethodRegistery + ); } return { diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index 89d5c54cbc2..25ea22ef274 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -11,6 +11,7 @@ import { mockManagementPlugin, existingDatasourceNamesList, mockDataSourceAttributesWithNoAuth, + mockDataSourceAttributesWithRegisteredAuth, } from '../../../../mocks'; import { OpenSearchDashboardsContextProvider } from '../../../../../../opensearch_dashboards_react/public'; import { EditDataSourceForm } from './edit_data_source_form'; @@ -344,9 +345,25 @@ describe('Datasource Management: Edit Datasource Form', () => { describe('With Registered Authentication', () => { let component: ReactWrapper, React.Component<{}, {}, any>>; - const mockCredentialForm = jest.fn(); + const updateInputFieldAndBlur = ( + comp: ReactWrapper, React.Component<{}, {}, any>>, + fieldName: string, + updatedValue: string, + isTestSubj?: boolean + ) => { + const field = isTestSubj ? comp.find(fieldName) : comp.find({ name: fieldName }); + act(() => { + field.last().simulate('change', { target: { value: updatedValue } }); + }); + comp.update(); + act(() => { + field.last().simulate('focus').simulate('blur'); + }); + comp.update(); + }; test('should call registered crendential form', () => { + const mockedCredentialForm = jest.fn(); const authTypeToBeTested = 'Some Auth Type'; const authMethodToBeTest = { name: authTypeToBeTested, @@ -354,7 +371,7 @@ describe('With Registered Authentication', () => { value: authTypeToBeTested, inputDisplay: 'some input', }, - credentialForm: mockCredentialForm, + credentialForm: mockedCredentialForm, } as AuthenticationMethod; const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); @@ -380,6 +397,67 @@ describe('With Registered Authentication', () => { } ); - expect(mockCredentialForm).toHaveBeenCalled(); + expect(mockedCredentialForm).toHaveBeenCalled(); + }); + + test('should update the form with registered auth type on click save changes', async () => { + const mockedCredentialForm = jest.fn(); + const mockedSubmitHandler = jest.fn(); + const authMethodToBeTest = { + name: 'Some Auth Type', + credentialSourceOption: { + value: 'Some Auth Type', + inputDisplay: 'some input', + }, + credentialForm: mockedCredentialForm, + crendentialFormField: {}, + } as AuthenticationMethod; + + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTest); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + await new Promise((resolve) => + setTimeout(() => { + updateInputFieldAndBlur(component, descriptionFieldIdentifier, ''); + expect( + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + resolve(); + }, 100) + ); + await new Promise((resolve) => + setTimeout(() => { + /* Updated description*/ + updateInputFieldAndBlur(component, descriptionFieldIdentifier, 'testDescription'); + expect( + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + + expect(component.find('[data-test-subj="datasource-edit-saveButton"]').exists()).toBe(true); + component.find('[data-test-subj="datasource-edit-saveButton"]').first().simulate('click'); + expect(mockedSubmitHandler).toHaveBeenCalled(); + resolve(); + }, 100) + ); }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index ee46af35531..0089e8f12af 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -44,7 +44,7 @@ import { } from '../../../validation'; import { UpdatePasswordModal } from '../update_password_modal'; import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; -import { getDefaultAuthMethod } from '../../../utils'; +import { extractRegisteredAuthTypeCredentials, getDefaultAuthMethod } from '../../../utils'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; @@ -60,8 +60,12 @@ export interface EditDataSourceState { description: string; endpoint: string; auth: { - type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content; + type: AuthType | string; + credentials: + | UsernamePasswordTypedContent + | SigV4Content + | { [key: string]: string } + | undefined; }; showUpdatePasswordModal: boolean; showUpdateAwsCredentialModal: boolean; @@ -155,7 +159,8 @@ export class EditDataSourceForm extends React.Component { expect(getDefaultAuthMethod(authenticationMethodRegistery)?.name).toBe(AuthType.NoAuth); }); }); + + describe('Check extractRegisteredAuthTypeCredentials method', () => { + test('Should extract credential field successfully', () => { + const authTypeToBeTested = 'Some Auth Type'; + + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + crendentialFormField: { + userNameRegistered: '', + passWordRegistered: '', + }, + } as AuthenticationMethod; + + const mockedCredentialState = { + userName: 'some userName', + passWord: 'some password', + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + } as { [key: string]: string }; + + const expectExtractedAuthCredentials = { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }; + + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTested); + + const registedAuthTypeCredentials = extractRegisteredAuthTypeCredentials( + mockedCredentialState, + authTypeToBeTested, + authenticationMethodRegistery + ); + + expect(deepEqual(registedAuthTypeCredentials, expectExtractedAuthCredentials)); + }); + + test('Should extract empty object when no credentialFormField registered ', () => { + const authTypeToBeTested = 'Some Auth Type'; + + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + } as AuthenticationMethod; + + const mockedCredentialState = { + userName: 'some userName', + passWord: 'some password', + } as { [key: string]: string }; + + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTested); + + const registedAuthTypeCredentials = extractRegisteredAuthTypeCredentials( + mockedCredentialState, + authTypeToBeTested, + authenticationMethodRegistery + ); + + expect(deepEqual(registedAuthTypeCredentials, {})); + }); + + test('Should fill in empty value when credentail state not have registered field', () => { + const authTypeToBeTested = 'Some Auth Type'; + + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + crendentialFormField: { + userNameRegistered: '', + passWordRegistered: '', + }, + } as AuthenticationMethod; + + const mockedCredentialState = { + userName: 'some userName', + passWord: 'some password', + userNameRegistered: 'some filled in userName from registed auth credential form', + } as { [key: string]: string }; + + const expectExtractedAuthCredentials = { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: '', + }; + + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTested); + + const registedAuthTypeCredentials = extractRegisteredAuthTypeCredentials( + mockedCredentialState, + authTypeToBeTested, + authenticationMethodRegistery + ); + + expect(deepEqual(registedAuthTypeCredentials, expectExtractedAuthCredentials)); + }); + }); }); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 13709f60657..871abc1dc63 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -143,3 +143,19 @@ export const getDefaultAuthMethod = ( return initialSelectedAuthMethod; }; + +export const extractRegisteredAuthTypeCredentials = ( + currentCredentialState: { [key: string]: string }, + authType: string, + authenticationMethodRegistery: AuthenticationMethodRegistery +) => { + const registeredCredentials = {} as { [key: string]: string }; + const registeredCredentialField = + authenticationMethodRegistery.getAuthenticationMethod(authType)?.crendentialFormField ?? {}; + + Object.keys(registeredCredentialField).forEach((credentialFiled) => { + registeredCredentials[credentialFiled] = currentCredentialState[credentialFiled] ?? ''; + }); + + return registeredCredentials; +}; diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts index 9a00d0f29c9..fbac18c7ddc 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts @@ -8,9 +8,11 @@ import { CreateDataSourceState } from '../create_data_source_wizard/components/c import { EditDataSourceState } from '../edit_data_source/components/edit_form/edit_data_source_form'; import { defaultValidation, performDataSourceFormValidation } from './datasource_form_validation'; import { mockDataSourceAttributesWithAuth } from '../../mocks'; +import { AuthenticationMethod, AuthenticationMethodRegistery } from '../../auth_registry'; describe('DataSourceManagement: Form Validation', () => { describe('validate create/edit datasource', () => { + let authenticationMethodRegistery = new AuthenticationMethodRegistery(); let form: CreateDataSourceState | EditDataSourceState = { formErrorsByField: { ...defaultValidation }, title: '', @@ -25,35 +27,40 @@ describe('DataSourceManagement: Form Validation', () => { }, }; test('should fail validation when title is empty', () => { - const result = performDataSourceFormValidation(form, [], ''); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistery); expect(result).toBe(false); }); test('should fail validation on duplicate title', () => { form.title = 'test'; - const result = performDataSourceFormValidation(form, ['oldTitle', 'test'], 'oldTitle'); + const result = performDataSourceFormValidation( + form, + ['oldTitle', 'test'], + 'oldTitle', + authenticationMethodRegistery + ); expect(result).toBe(false); }); test('should fail validation when endpoint is not valid', () => { form.endpoint = mockDataSourceAttributesWithAuth.endpoint; - const result = performDataSourceFormValidation(form, [], ''); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistery); expect(result).toBe(false); }); test('should fail validation when username is empty', () => { form.endpoint = 'test'; - const result = performDataSourceFormValidation(form, [], ''); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistery); expect(result).toBe(false); }); test('should fail validation when password is empty', () => { form.auth.credentials.username = 'test'; form.auth.credentials.password = ''; - const result = performDataSourceFormValidation(form, [], ''); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistery); expect(result).toBe(false); }); test('should NOT fail validation on empty username/password when No Auth is selected', () => { form.auth.type = AuthType.NoAuth; form.title = 'test'; form.endpoint = mockDataSourceAttributesWithAuth.endpoint; - const result = performDataSourceFormValidation(form, [], ''); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistery); expect(result).toBe(true); }); test('should NOT fail validation on all fields', () => { @@ -61,7 +68,46 @@ describe('DataSourceManagement: Form Validation', () => { const result = performDataSourceFormValidation( form, [mockDataSourceAttributesWithAuth.title], - mockDataSourceAttributesWithAuth.title + mockDataSourceAttributesWithAuth.title, + authenticationMethodRegistery + ); + expect(result).toBe(true); + }); + test('should NOT fail validation when registered auth type is selected and related credential field not empty', () => { + authenticationMethodRegistery = new AuthenticationMethodRegistery(); + const authMethodToBeTested = { + name: 'Some Auth Type', + credentialSourceOption: { + value: 'Some Auth Type', + inputDisplay: 'some input', + }, + credentialForm: jest.fn(), + crendentialFormField: { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }, + } as AuthenticationMethod; + + authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTested); + + const formWithRegisteredAuth: CreateDataSourceState | EditDataSourceState = { + formErrorsByField: { ...defaultValidation }, + title: 'test registered auth type', + description: '', + endpoint: 'https://test.com', + auth: { + type: 'Some Auth Type', + credentials: { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }, + }, + }; + const result = performDataSourceFormValidation( + formWithRegisteredAuth, + [], + '', + authenticationMethodRegistery ); expect(result).toBe(true); }); diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts index aecf6e51730..2ae5585e181 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts @@ -4,10 +4,11 @@ */ import { i18n } from '@osd/i18n'; -import { isValidUrl } from '../utils'; +import { extractRegisteredAuthTypeCredentials, isValidUrl } from '../utils'; import { CreateDataSourceState } from '../create_data_source_wizard/components/create_form/create_data_source_form'; import { EditDataSourceState } from '../edit_data_source/components/edit_form/edit_data_source_form'; import { AuthType } from '../../types'; +import { AuthenticationMethodRegistery } from '../../auth_registry'; export interface CreateEditDataSourceValidation { title: string[]; @@ -68,7 +69,8 @@ export const isTitleValid = ( export const performDataSourceFormValidation = ( formValues: CreateDataSourceState | EditDataSourceState, existingDatasourceNamesList: string[], - existingTitle: string + existingTitle: string, + authenticationMethodRegistery: AuthenticationMethodRegistery ) => { /* Title validation */ const titleValid = isTitleValid(formValues?.title, existingDatasourceNamesList, existingTitle); @@ -84,8 +86,9 @@ export const performDataSourceFormValidation = ( /* Credential Validation */ - /* Username & Password */ - if (formValues?.auth?.type === AuthType.UsernamePasswordType) { + if (formValues?.auth?.type === AuthType.NoAuth) { + return true; + } else if (formValues?.auth?.type === AuthType.UsernamePasswordType) { /* Username */ if (!formValues.auth.credentials?.username) { return false; @@ -95,9 +98,7 @@ export const performDataSourceFormValidation = ( if (!formValues.auth.credentials?.password) { return false; } - } - /* AWS SigV4 Content */ - if (formValues?.auth?.type === AuthType.SigV4) { + } else if (formValues?.auth?.type === AuthType.SigV4) { /* Access key */ if (!formValues.auth.credentials?.accessKey) { return false; @@ -117,6 +118,18 @@ export const performDataSourceFormValidation = ( if (!formValues.auth.credentials?.service) { return false; } + } else { + const registeredCredentials = extractRegisteredAuthTypeCredentials( + (formValues?.auth?.credentials ?? {}) as { [key: string]: string }, + formValues?.auth?.type ?? '', + authenticationMethodRegistery + ); + + for (const credentialValue of Object.values(registeredCredentials)) { + if (credentialValue.trim().length === 0) { + return false; + } + } } return true; diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 628cd368a91..2d539cf19e1 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -6,7 +6,7 @@ import React from 'react'; import { throwError } from 'rxjs'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import { AuthType } from './types'; +import { AuthType, DataSourceAttributes } from './types'; import { coreMock } from '../../../core/public/mocks'; import { DataSourceManagementPlugin, @@ -204,6 +204,18 @@ export const mockDataSourceAttributesWithNoAuth = { credentials: undefined, }, }; + +export const mockDataSourceAttributesWithRegisteredAuth = { + id: 'testRegisteredAuth', + title: 'create-test-ds-registered-auth', + description: 'jest testing', + endpoint: 'https://test.com', + auth: { + type: 'Some Auth Type', + credentials: {} as { [key: string]: string }, + }, +} as DataSourceAttributes; + export const getDataSourceByIdWithCredential = { attributes: { id: 'alpha-test', diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index b0dd6caf1a1..d8df61d304b 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -121,7 +121,7 @@ export const sigV4CredentialField = { region: '', accessKey: '', secretKey: '', - service: '', + service: SigV4ServiceName.OpenSearch, }; export const sigV4AuthMethod = { @@ -141,8 +141,12 @@ export interface DataSourceAttributes extends SavedObjectAttributes { description?: string; endpoint?: string; auth: { - type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + type: AuthType | string; + credentials: + | UsernamePasswordTypedContent + | SigV4Content + | { [key: string]: string } + | undefined; }; }