From 5d9dea7fb5ac51ce32d1b62f1437f47dd72408f3 Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 08:24:56 +0100 Subject: [PATCH 1/9] add password error to autentication default --- .../authentication-default.spec.tsx | 18 ++++++++++++++++-- .../authentication-default.tsx | 12 +++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx index 650f9e8684a..90902c8699f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx @@ -171,12 +171,26 @@ describe('AuthenticationDefault Component', function () { errors: [ { fieldName: 'username', - message: 'pineapples', + message: 'username error', }, ], updateConnectionFormField: updateConnectionFormFieldSpy, }); - expect(screen.getByText('pineapples')).to.be.visible; + expect(screen.getByText('username error')).to.be.visible; + }); + + it('renders a password error when there is a password error', function () { + renderComponent({ + errors: [ + { + fieldName: 'password', + message: 'password error', + }, + ], + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + + expect(screen.getByText('password error')).to.be.visible; }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx index d615f171a04..07ebab73fe2 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx @@ -14,7 +14,10 @@ import { AuthMechanism } from 'mongodb'; import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; import FormFieldContainer from '../../form-field-container'; -import { ConnectionFormError } from '../../../utils/validation'; +import { + ConnectionFormError, + errorMessageByFieldName, +} from '../../../utils/validation'; const authSourceLabelStyles = css({ padding: 0, @@ -76,7 +79,8 @@ function AuthenticationDefault({ [updateConnectionFormField] ); - const usernameError = errors?.find((error) => error.fieldName === 'username'); + const usernameError = errorMessageByFieldName(errors, 'username'); + const passwordError = errorMessageByFieldName(errors, 'password'); return ( <> @@ -91,7 +95,7 @@ function AuthenticationDefault({ }); }} label="Username" - errorMessage={usernameError?.message} + errorMessage={usernameError} state={usernameError ? 'error' : undefined} value={username || ''} /> @@ -109,6 +113,8 @@ function AuthenticationDefault({ label="Password" type="password" value={password || ''} + errorMessage={passwordError} + state={passwordError ? 'error' : undefined} /> From 11d71c7b5908b2919a49de4883bc929777d72d2e Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 10:37:43 +0100 Subject: [PATCH 2/9] feat(connect-form): kerberos auth tab --- .../authentication-gssapi.tsx | 127 +++++++++++++++++- .../src/hooks/use-connect-form.spec.ts | 27 +++- .../src/hooks/use-connect-form.ts | 61 ++++++++- .../connect-form/src/utils/url-options.ts | 4 - 4 files changed, 207 insertions(+), 12 deletions(-) diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index 4c1cb74e23d..699ea0652c4 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -1,9 +1,132 @@ import React from 'react'; +import { css, spacing, TextInput, Toggle } from '@mongodb-js/compass-components'; + +import ConnectionStringUrl from 'mongodb-connection-string-url'; +import { + parseAuthMechanismProperties, + UpdateConnectionFormField, +} from '../../../hooks/use-connect-form'; +import FormFieldContainer from '../../form-field-container'; +import { + ConnectionFormError, + errorMessageByFieldName, +} from '../../../utils/validation'; + +const canonicalizeHostnameStyles = css({ + padding: 0, + margin: 0, + display: 'flex', +}); + +const canonicalizeHostnameToggleStyles = css({ + height: 14, + width: 26, + margin: spacing[1], + marginRight: 0, + marginLeft: 'auto', +}); + +const canonicalizeHostnameLabelStyles = css({ + '&:hover': { + cursor: 'pointer', + }, +}); + +function AuthenticationGSSAPI({ + errors, + connectionStringUrl, + updateConnectionFormField, +}: { + connectionStringUrl: ConnectionStringUrl; + errors: ConnectionFormError[]; + updateConnectionFormField: UpdateConnectionFormField; +}): React.ReactElement { + const kerberosPrincipalError = errorMessageByFieldName( + errors, + 'kerberosPrincipal' + ); + const principal = decodeURIComponent(connectionStringUrl.username); + const authMechanismProperties = + parseAuthMechanismProperties(connectionStringUrl); + const serviceName = authMechanismProperties.get('SERVICE_NAME'); + const serviceRealm = authMechanismProperties.get('SERVICE_REALM'); + const canonicalizeHostname = authMechanismProperties.get('CANONICALIZE_HOST_NAME'); -function AuthenticationGSSAPI(): React.ReactElement { return ( <> -

Kerberos

+ + ) => { + updateConnectionFormField({ + type: 'update-username', + username: value, + }); + }} + label="Principal" + errorMessage={kerberosPrincipalError} + state={kerberosPrincipalError ? 'error' : undefined} + value={principal || ''} + /> + + + + ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'SERVICE_NAME', + value: value, + }); + }} + label="Service Name" + value={serviceName || ''} + /> + + + +
+ + { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'CANONICALIZE_HOST_NAME', + value: checked ? 'true' : '', + }); + }} + checked={canonicalizeHostname === 'true'} + /> +
+
+ + + ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'SERVICE_REALM', + value: value, + }); + }} + label="Service Realm" + value={serviceRealm || ''} + /> + ); } diff --git a/packages/connect-form/src/hooks/use-connect-form.spec.ts b/packages/connect-form/src/hooks/use-connect-form.spec.ts index c8f7d0c3d2e..f934f9d72b9 100644 --- a/packages/connect-form/src/hooks/use-connect-form.spec.ts +++ b/packages/connect-form/src/hooks/use-connect-form.spec.ts @@ -1,6 +1,9 @@ import { expect } from 'chai'; import ConnectionStringUrl from 'mongodb-connection-string-url'; -import { handleConnectionFormFieldUpdate } from './use-connect-form'; +import { + handleConnectionFormFieldUpdate, + parseAuthMechanismProperties, +} from './use-connect-form'; describe('use-connect-form hook', function () { describe('#handleConnectionFormFieldUpdate', function () { @@ -822,4 +825,26 @@ describe('use-connect-form hook', function () { }); }); }); + + describe('parseAuthMechanismProperties', function () { + it('parses legit authMechanismProperties string', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal('serviceName'); + }); + + it('parses broken authMechanismProperties string as empty', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=broken' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal(undefined); + }); + }); }); diff --git a/packages/connect-form/src/hooks/use-connect-form.ts b/packages/connect-form/src/hooks/use-connect-form.ts index c937679eb7b..5f494e5931c 100644 --- a/packages/connect-form/src/hooks/use-connect-form.ts +++ b/packages/connect-form/src/hooks/use-connect-form.ts @@ -1,9 +1,10 @@ import { Dispatch, useCallback, useEffect, useReducer } from 'react'; -import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionInfo, ConnectionOptions } from 'mongodb-data-service'; import type { MongoClientOptions, ProxyOptions } from 'mongodb'; import { cloneDeep } from 'lodash'; -import ConnectionString from 'mongodb-connection-string-url'; +import ConnectionStringUrl, { + CommaAndColonSeparatedRecord, +} from 'mongodb-connection-string-url'; import { ConnectionFormError, @@ -35,7 +36,6 @@ import { UpdatePasswordAction, UpdateUsernameAction, } from '../utils/authentication-handler'; - export interface ConnectFormState { connectionOptions: ConnectionOptions; enableEditingConnectionString: boolean; @@ -43,6 +43,13 @@ export interface ConnectFormState { warnings: ConnectionFormWarning[]; } +interface AuthMechanismProperties { + SERVICE_NAME?: string; + SERVICE_REALM?: string; + CANONICALIZE_HOST_NAME?: boolean; + AWS_SESSION_TOKEN?: string; +} + type Action = | { type: 'set-connection-form-state'; @@ -117,12 +124,17 @@ type ConnectionFormFieldActions = type: 'update-search-param'; currentKey: keyof MongoClientOptions; newKey?: keyof MongoClientOptions; - value?: unknown; + value?: string; } | { type: 'delete-search-param'; key: keyof MongoClientOptions; } + | { + type: 'update-auth-mechanism-property'; + key: keyof AuthMechanismProperties; + value?: string; + } | { type: 'update-connection-path'; value: string; @@ -140,7 +152,7 @@ export type UpdateConnectionFormField = ( function parseConnectionString( connectionString: string -): [ConnectionString | undefined, ConnectionFormError[]] { +): [ConnectionStringUrl | undefined, ConnectionFormError[]] { const [parsedConnectionString, parsingError] = tryToParseConnectionString(connectionString); @@ -412,6 +424,29 @@ export function handleConnectionFormFieldUpdate( }, }; } + case 'update-auth-mechanism-property': { + const authMechanismProperties = parseAuthMechanismProperties( + parsedConnectionStringUrl + ); + + if (action.value) { + authMechanismProperties.set(action.key, action.value); + } else { + authMechanismProperties.delete(action.key); + } + + updatedSearchParams.set( + 'authMechanismProperties', + authMechanismProperties.toString() + ); + + return { + connectionOptions: { + ...currentConnectionOptions, + connectionString: parsedConnectionStringUrl.toString(), + }, + }; + } case 'delete-search-param': { updatedSearchParams.delete(action.key); return { @@ -456,6 +491,22 @@ export function handleConnectionFormFieldUpdate( } } +export function parseAuthMechanismProperties( + connectionString: ConnectionStringUrl +): CommaAndColonSeparatedRecord { + const searchParams = connectionString.typedSearchParams(); + const authMechanismPropertiesString = searchParams.get( + 'authMechanismProperties' + ); + try { + return new CommaAndColonSeparatedRecord( + authMechanismPropertiesString + ); + } catch (e) { + return new CommaAndColonSeparatedRecord(); + } +} + export function useConnectForm( initialConnectionInfo: ConnectionInfo, connectionErrorMessage?: string | null diff --git a/packages/connect-form/src/utils/url-options.ts b/packages/connect-form/src/utils/url-options.ts index 67e839ff3e7..558a2c30180 100644 --- a/packages/connect-form/src/utils/url-options.ts +++ b/packages/connect-form/src/utils/url-options.ts @@ -36,10 +36,6 @@ export const editableUrlOptions = [ title: 'Read Preferences Options', values: ['maxStalenessSeconds', 'readPreferenceTags'], }, - { - title: 'Authentication Options', - values: ['authMechanismProperties', 'gssapiServiceName'], - }, { title: 'Server Options', values: [ From 1e06f0aef4ef5964447adf984d82d41c24c0050e Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 16:35:10 +0100 Subject: [PATCH 3/9] fix type query params types --- .../advanced-tab/advanced-tab.tsx | 2 +- .../authentication-gssapi.tsx | 60 +++++-------------- .../general-tab/direct-connection-input.tsx | 2 +- .../ssh-tunnel-tab/socks.tsx | 2 +- .../tls-ssl-tab/tls-ssl-tab.tsx | 7 ++- 5 files changed, 24 insertions(+), 49 deletions(-) diff --git a/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx index ddf23c17b22..0342dafe173 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx @@ -50,7 +50,7 @@ function AdvancedTab({ : pathname; const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index 699ea0652c4..c6786603e0f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { css, spacing, TextInput, Toggle } from '@mongodb-js/compass-components'; +import { Checkbox, TextInput } from '@mongodb-js/compass-components'; import ConnectionStringUrl from 'mongodb-connection-string-url'; import { @@ -12,26 +12,6 @@ import { errorMessageByFieldName, } from '../../../utils/validation'; -const canonicalizeHostnameStyles = css({ - padding: 0, - margin: 0, - display: 'flex', -}); - -const canonicalizeHostnameToggleStyles = css({ - height: 14, - width: 26, - margin: spacing[1], - marginRight: 0, - marginLeft: 'auto', -}); - -const canonicalizeHostnameLabelStyles = css({ - '&:hover': { - cursor: 'pointer', - }, -}); - function AuthenticationGSSAPI({ errors, connectionStringUrl, @@ -50,7 +30,9 @@ function AuthenticationGSSAPI({ parseAuthMechanismProperties(connectionStringUrl); const serviceName = authMechanismProperties.get('SERVICE_NAME'); const serviceRealm = authMechanismProperties.get('SERVICE_REALM'); - const canonicalizeHostname = authMechanismProperties.get('CANONICALIZE_HOST_NAME'); + const canonicalizeHostname = authMechanismProperties.get( + 'CANONICALIZE_HOST_NAME' + ); return ( <> @@ -88,28 +70,18 @@ function AuthenticationGSSAPI({
-
- - { - updateConnectionFormField({ - type: 'update-auth-mechanism-property', - key: 'CANONICALIZE_HOST_NAME', - value: checked ? 'true' : '', - }); - }} - checked={canonicalizeHostname === 'true'} - /> -
+ ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'CANONICALIZE_HOST_NAME', + value: event.target.checked ? 'true' : '', + }); + }} + label="Canonicalize Host Name" + checked={canonicalizeHostname === 'true'} + bold={false} + />
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx index cf65e85b27c..00f1bebecc2 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx @@ -28,7 +28,7 @@ function DirectConnectionInput({ return updateConnectionFormField({ type: 'update-search-param', currentKey: 'directConnection', - value: event.target.checked, + value: event.target.checked ? 'true' : 'false', }); }, [updateConnectionFormField] diff --git a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx index b8b519fb9d3..a3707f9659f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx @@ -38,7 +38,7 @@ function Socks({ connectionStringUrl.typedSearchParams(); const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx index 4ea2bf545f1..769b91ff229 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx @@ -144,7 +144,7 @@ function TLSTab({ const tlsOptionsDisabled = tlsOption !== 'ON'; const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string | null) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', @@ -212,7 +212,10 @@ function TLSTab({ ) => { - handleFieldChanged(tlsOptionField.name, event.target.checked); + handleFieldChanged( + tlsOptionField.name, + event.target.checked ? 'true' : 'false' + ); }} data-testid={`${tlsOptionField.name}-input`} label={tlsOptionField.name} From 6505f1982f2b5efeeb888b6ae7b14e6b4c79d051 Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 16:59:44 +0100 Subject: [PATCH 4/9] fix tests and extract auth mechanism properties utils --- .../direct-connection-input.spec.tsx | 4 +-- .../tls-ssl-tab/tls-ssl-tab.spec.tsx | 2 +- .../tls-ssl-tab/tls-ssl-tab.tsx | 2 +- .../src/hooks/use-connect-form.spec.ts | 27 +--------------- .../src/hooks/use-connect-form.ts | 32 +++---------------- .../utils/auth-mechanism-properties.spec.ts | 25 +++++++++++++++ .../src/utils/auth-mechanism-properties.ts | 27 ++++++++++++++++ 7 files changed, 62 insertions(+), 57 deletions(-) create mode 100644 packages/connect-form/src/utils/auth-mechanism-properties.spec.ts create mode 100644 packages/connect-form/src/utils/auth-mechanism-properties.ts diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx index 0a0b0cc8949..0ad2b20e4d6 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx @@ -78,7 +78,7 @@ describe('DirectConnectionInput', function () { expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ type: 'update-search-param', currentKey: 'directConnection', - value: true, + value: 'true', }); }); }); @@ -113,7 +113,7 @@ describe('DirectConnectionInput', function () { expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ type: 'update-search-param', currentKey: 'directConnection', - value: true, + value: 'true', }); }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx index 3e1bf3b59b4..63794ee7cb1 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx @@ -248,7 +248,7 @@ describe('SchemaInput', function () { ).to.deep.equal({ type: 'update-search-param', currentKey: connectionStringTlsParam, - value: true, + value: 'true', }); }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx index 769b91ff229..a8858b7f4be 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx @@ -214,7 +214,7 @@ function TLSTab({ onChange={(event: React.ChangeEvent) => { handleFieldChanged( tlsOptionField.name, - event.target.checked ? 'true' : 'false' + event.target.checked ? 'true' : '' ); }} data-testid={`${tlsOptionField.name}-input`} diff --git a/packages/connect-form/src/hooks/use-connect-form.spec.ts b/packages/connect-form/src/hooks/use-connect-form.spec.ts index f934f9d72b9..c8f7d0c3d2e 100644 --- a/packages/connect-form/src/hooks/use-connect-form.spec.ts +++ b/packages/connect-form/src/hooks/use-connect-form.spec.ts @@ -1,9 +1,6 @@ import { expect } from 'chai'; import ConnectionStringUrl from 'mongodb-connection-string-url'; -import { - handleConnectionFormFieldUpdate, - parseAuthMechanismProperties, -} from './use-connect-form'; +import { handleConnectionFormFieldUpdate } from './use-connect-form'; describe('use-connect-form hook', function () { describe('#handleConnectionFormFieldUpdate', function () { @@ -825,26 +822,4 @@ describe('use-connect-form hook', function () { }); }); }); - - describe('parseAuthMechanismProperties', function () { - it('parses legit authMechanismProperties string', function () { - const props = parseAuthMechanismProperties( - new ConnectionStringUrl( - 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' - ) - ); - - expect(props.get('SERVICE_NAME')).to.equal('serviceName'); - }); - - it('parses broken authMechanismProperties string as empty', function () { - const props = parseAuthMechanismProperties( - new ConnectionStringUrl( - 'mongodb://localhost?authMechanismProperties=broken' - ) - ); - - expect(props.get('SERVICE_NAME')).to.equal(undefined); - }); - }); }); diff --git a/packages/connect-form/src/hooks/use-connect-form.ts b/packages/connect-form/src/hooks/use-connect-form.ts index 5f494e5931c..6723abbfd45 100644 --- a/packages/connect-form/src/hooks/use-connect-form.ts +++ b/packages/connect-form/src/hooks/use-connect-form.ts @@ -2,10 +2,7 @@ import { Dispatch, useCallback, useEffect, useReducer } from 'react'; import { ConnectionInfo, ConnectionOptions } from 'mongodb-data-service'; import type { MongoClientOptions, ProxyOptions } from 'mongodb'; import { cloneDeep } from 'lodash'; -import ConnectionStringUrl, { - CommaAndColonSeparatedRecord, -} from 'mongodb-connection-string-url'; - +import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionFormError, ConnectionFormWarning, @@ -36,6 +33,10 @@ import { UpdatePasswordAction, UpdateUsernameAction, } from '../utils/authentication-handler'; +import { + AuthMechanismProperties, + parseAuthMechanismProperties, +} from '../utils/auth-mechanism-properties'; export interface ConnectFormState { connectionOptions: ConnectionOptions; enableEditingConnectionString: boolean; @@ -43,13 +44,6 @@ export interface ConnectFormState { warnings: ConnectionFormWarning[]; } -interface AuthMechanismProperties { - SERVICE_NAME?: string; - SERVICE_REALM?: string; - CANONICALIZE_HOST_NAME?: boolean; - AWS_SESSION_TOKEN?: string; -} - type Action = | { type: 'set-connection-form-state'; @@ -491,22 +485,6 @@ export function handleConnectionFormFieldUpdate( } } -export function parseAuthMechanismProperties( - connectionString: ConnectionStringUrl -): CommaAndColonSeparatedRecord { - const searchParams = connectionString.typedSearchParams(); - const authMechanismPropertiesString = searchParams.get( - 'authMechanismProperties' - ); - try { - return new CommaAndColonSeparatedRecord( - authMechanismPropertiesString - ); - } catch (e) { - return new CommaAndColonSeparatedRecord(); - } -} - export function useConnectForm( initialConnectionInfo: ConnectionInfo, connectionErrorMessage?: string | null diff --git a/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts b/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts new file mode 100644 index 00000000000..88acc9ae09c --- /dev/null +++ b/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import ConnectionStringUrl from 'mongodb-connection-string-url'; +import { parseAuthMechanismProperties } from './auth-mechanism-properties'; + +describe('parseAuthMechanismProperties', function () { + it('parses legit authMechanismProperties string', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal('serviceName'); + }); + + it('parses broken authMechanismProperties string as empty', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=broken' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal(undefined); + }); +}); diff --git a/packages/connect-form/src/utils/auth-mechanism-properties.ts b/packages/connect-form/src/utils/auth-mechanism-properties.ts new file mode 100644 index 00000000000..a5fa2112e5b --- /dev/null +++ b/packages/connect-form/src/utils/auth-mechanism-properties.ts @@ -0,0 +1,27 @@ +import type { MongoClientOptions } from 'mongodb'; +import ConnectionStringUrl, { + CommaAndColonSeparatedRecord, +} from 'mongodb-connection-string-url'; + +export interface AuthMechanismProperties { + SERVICE_NAME?: string; + SERVICE_REALM?: string; + CANONICALIZE_HOST_NAME?: boolean; + AWS_SESSION_TOKEN?: string; +} + +export function parseAuthMechanismProperties( + connectionString: ConnectionStringUrl +): CommaAndColonSeparatedRecord { + const searchParams = connectionString.typedSearchParams(); + const authMechanismPropertiesString = searchParams.get( + 'authMechanismProperties' + ); + try { + return new CommaAndColonSeparatedRecord( + authMechanismPropertiesString + ); + } catch (e) { + return new CommaAndColonSeparatedRecord(); + } +} From 5839b2a461809667e3cf01906592acb4ee588800 Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 19:43:31 +0100 Subject: [PATCH 5/9] add $external automatically + tests --- .../authentication-gssapi.spec.tsx | 133 ++++++++++++++++++ .../authentication-gssapi.tsx | 6 +- .../src/utils/authentication-handler.spec.ts | 56 +++++++- .../src/utils/authentication-handler.ts | 7 + 4 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx new file mode 100644 index 00000000000..c9e7ed0c66a --- /dev/null +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import ConnectionStringUrl from 'mongodb-connection-string-url'; + +import AuthenticationGssapi from './authentication-gssapi'; +import { ConnectionFormError } from '../../../utils/validation'; +import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; + +function renderComponent({ + errors = [], + connectionStringUrl = new ConnectionStringUrl('mongodb://localhost:27017'), + updateConnectionFormField, +}: { + connectionStringUrl?: ConnectionStringUrl; + errors?: ConnectionFormError[]; + updateConnectionFormField: UpdateConnectionFormField; +}) { + render( + + ); +} + +describe('AuthenticationGssapi Component', function () { + let updateConnectionFormFieldSpy: sinon.SinonSpy; + beforeEach(function () { + updateConnectionFormFieldSpy = sinon.spy(); + }); + + describe('when the kerberosPrincipal input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getAllByRole('textbox')[0], { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + type: 'update-username', + username: 'good sandwich', + }); + }); + }); + + describe('when the serviceName input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getAllByRole('textbox')[1], { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'SERVICE_NAME', + type: 'update-auth-mechanism-property', + value: 'good sandwich', + }); + }); + }); + + describe('when the serviceRealm input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getAllByRole('textbox')[2], { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'SERVICE_REALM', + type: 'update-auth-mechanism-property', + value: 'good sandwich', + }); + }); + }); + + describe('when the canoncalize hostname is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.click(screen.getByRole('checkbox')); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'CANONICALIZE_HOST_NAME', + type: 'update-auth-mechanism-property', + value: 'true', + }); + }); + }); + + it('renders an error when there is a kerberosPrincipal error', function () { + renderComponent({ + errors: [ + { + fieldName: 'kerberosPrincipal', + message: 'kerberosPrincipal error', + }, + ], + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + + expect(screen.getByText('kerberosPrincipal error')).to.be.visible; + }); +}); diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index c6786603e0f..3a6be9177b6 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -2,15 +2,13 @@ import React from 'react'; import { Checkbox, TextInput } from '@mongodb-js/compass-components'; import ConnectionStringUrl from 'mongodb-connection-string-url'; -import { - parseAuthMechanismProperties, - UpdateConnectionFormField, -} from '../../../hooks/use-connect-form'; +import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; import FormFieldContainer from '../../form-field-container'; import { ConnectionFormError, errorMessageByFieldName, } from '../../../utils/validation'; +import { parseAuthMechanismProperties } from '../../../utils/auth-mechanism-properties'; function AuthenticationGSSAPI({ errors, diff --git a/packages/connect-form/src/utils/authentication-handler.spec.ts b/packages/connect-form/src/utils/authentication-handler.spec.ts index 2b08f5d3b54..67871bf7c11 100644 --- a/packages/connect-form/src/utils/authentication-handler.spec.ts +++ b/packages/connect-form/src/utils/authentication-handler.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { MongoClientOptions } from 'mongodb'; +import { AuthMechanism, MongoClientOptions } from 'mongodb'; import ConnectionString from 'mongodb-connection-string-url'; import { @@ -230,11 +230,63 @@ describe('Authentication Handler', function () { }); expect(res.connectionOptions.connectionString).to.equal( - 'mongodb://localhost/?authMechanism=PLAIN' + 'mongodb://localhost/?authMechanism=PLAIN&authSource=%24external' ); expect(res.errors).to.equal(undefined); }); + it('should set authSource=$external when needed', function () { + const externalAuthMechanisms: AuthMechanism[] = [ + 'MONGODB-AWS', + 'GSSAPI', + 'PLAIN', + 'MONGODB-X509', + ]; + + for (const authMechanism of externalAuthMechanisms) { + const res = handleUpdateAuthMechanism({ + action: { + type: 'update-auth-mechanism', + authMechanism, + }, + connectionStringUrl: new ConnectionString(testConnectionString), + connectionOptions: { + connectionString: testConnectionString, + }, + }); + + expect(res.connectionOptions.connectionString).to.equal( + `mongodb://localhost/?authMechanism=${authMechanism}&authSource=%24external` + ); + } + }); + + it('should not set authSource=$external when not needed', function () { + const externalAuthMechanisms: AuthMechanism[] = [ + 'MONGODB-CR', + 'DEFAULT', + 'SCRAM-SHA-1', + 'SCRAM-SHA-256', + ]; + + for (const authMechanism of externalAuthMechanisms) { + const res = handleUpdateAuthMechanism({ + action: { + type: 'update-auth-mechanism', + authMechanism, + }, + connectionStringUrl: new ConnectionString(testConnectionString), + connectionOptions: { + connectionString: testConnectionString, + }, + }); + + expect(res.connectionOptions.connectionString).to.equal( + `mongodb://localhost/?authMechanism=${authMechanism}` + ); + } + }); + it('should remove the username/password field when being set to no auth', function () { const res = handleUpdateAuthMechanism({ action: { diff --git a/packages/connect-form/src/utils/authentication-handler.ts b/packages/connect-form/src/utils/authentication-handler.ts index 6b7ce770e62..bc9d084415c 100644 --- a/packages/connect-form/src/utils/authentication-handler.ts +++ b/packages/connect-form/src/utils/authentication-handler.ts @@ -60,6 +60,13 @@ export function handleUpdateAuthMechanism({ if (action.authMechanism) { updatedSearchParams.set('authMechanism', action.authMechanism); + if ( + ['MONGODB-AWS', 'GSSAPI', 'PLAIN', 'MONGODB-X509'].includes( + action.authMechanism + ) + ) { + updatedSearchParams.set('authSource', '$external'); + } } return { From 59ce2c797a3c73a2cc2ae5a98b9de4c93b22d12f Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 20:17:19 +0100 Subject: [PATCH 6/9] extract fn to manipulate connection string in one place --- .../authentication-default.spec.tsx | 4 +- .../authentication-default.tsx | 5 +- .../authentication-gssapi.tsx | 4 +- .../src/hooks/use-connect-form.ts | 5 +- .../utils/auth-mechanism-properties.spec.ts | 25 ---- .../src/utils/auth-mechanism-properties.ts | 27 ---- .../src/utils/authentication-handler.ts | 21 ++- .../utils/connection-string-helpers.spec.ts | 133 ++++++++++++++++++ .../src/utils/connection-string-helpers.ts | 68 +++++++++ .../connect-form/src/utils/validation.spec.ts | 34 ----- packages/connect-form/src/utils/validation.ts | 11 -- 11 files changed, 225 insertions(+), 112 deletions(-) delete mode 100644 packages/connect-form/src/utils/auth-mechanism-properties.spec.ts delete mode 100644 packages/connect-form/src/utils/auth-mechanism-properties.ts create mode 100644 packages/connect-form/src/utils/connection-string-helpers.spec.ts create mode 100644 packages/connect-form/src/utils/connection-string-helpers.ts diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx index 90902c8699f..0194e9bb644 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx @@ -156,12 +156,12 @@ describe('AuthenticationDefault Component', function () { it('decodes the password as a uri component before rendering', function () { renderComponent({ connectionStringUrl: new ConnectionStringUrl( - 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345' + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345' ), updateConnectionFormField: updateConnectionFormFieldSpy, }); - expect(screen.getByLabelText('Username').getAttribute('value')).to.equal( + expect(screen.getByLabelText('Password').getAttribute('value')).to.equal( 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' ); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx index 07ebab73fe2..d8cd3dd4601 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx @@ -18,6 +18,7 @@ import { ConnectionFormError, errorMessageByFieldName, } from '../../../utils/validation'; +import { getConnectionStringPassword, getConnectionStringUsername } from '../../../utils/connection-string-helpers'; const authSourceLabelStyles = css({ padding: 0, @@ -57,8 +58,8 @@ function AuthenticationDefault({ errors: ConnectionFormError[]; updateConnectionFormField: UpdateConnectionFormField; }): React.ReactElement { - const password = decodeURIComponent(connectionStringUrl.password); - const username = decodeURIComponent(connectionStringUrl.username); + const password = getConnectionStringPassword(connectionStringUrl); + const username = getConnectionStringUsername(connectionStringUrl); const selectedAuthMechanism = connectionStringUrl.searchParams.get('authMechanism') ?? ''; diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index 3a6be9177b6..2cef77a7a20 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -8,7 +8,7 @@ import { ConnectionFormError, errorMessageByFieldName, } from '../../../utils/validation'; -import { parseAuthMechanismProperties } from '../../../utils/auth-mechanism-properties'; +import { getConnectionStringUsername, parseAuthMechanismProperties } from '../../../utils/connection-string-helpers'; function AuthenticationGSSAPI({ errors, @@ -23,7 +23,7 @@ function AuthenticationGSSAPI({ errors, 'kerberosPrincipal' ); - const principal = decodeURIComponent(connectionStringUrl.username); + const principal = getConnectionStringUsername(connectionStringUrl); const authMechanismProperties = parseAuthMechanismProperties(connectionStringUrl); const serviceName = authMechanismProperties.get('SERVICE_NAME'); diff --git a/packages/connect-form/src/hooks/use-connect-form.ts b/packages/connect-form/src/hooks/use-connect-form.ts index 6723abbfd45..8ba1331c314 100644 --- a/packages/connect-form/src/hooks/use-connect-form.ts +++ b/packages/connect-form/src/hooks/use-connect-form.ts @@ -6,7 +6,6 @@ import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionFormError, ConnectionFormWarning, - tryToParseConnectionString, validateConnectionOptionsWarnings, } from '../utils/validation'; import { getNextHost } from '../utils/get-next-host'; @@ -36,7 +35,9 @@ import { import { AuthMechanismProperties, parseAuthMechanismProperties, -} from '../utils/auth-mechanism-properties'; + tryToParseConnectionString, +} from '../utils/connection-string-helpers'; + export interface ConnectFormState { connectionOptions: ConnectionOptions; enableEditingConnectionString: boolean; diff --git a/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts b/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts deleted file mode 100644 index 88acc9ae09c..00000000000 --- a/packages/connect-form/src/utils/auth-mechanism-properties.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from 'chai'; -import ConnectionStringUrl from 'mongodb-connection-string-url'; -import { parseAuthMechanismProperties } from './auth-mechanism-properties'; - -describe('parseAuthMechanismProperties', function () { - it('parses legit authMechanismProperties string', function () { - const props = parseAuthMechanismProperties( - new ConnectionStringUrl( - 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' - ) - ); - - expect(props.get('SERVICE_NAME')).to.equal('serviceName'); - }); - - it('parses broken authMechanismProperties string as empty', function () { - const props = parseAuthMechanismProperties( - new ConnectionStringUrl( - 'mongodb://localhost?authMechanismProperties=broken' - ) - ); - - expect(props.get('SERVICE_NAME')).to.equal(undefined); - }); -}); diff --git a/packages/connect-form/src/utils/auth-mechanism-properties.ts b/packages/connect-form/src/utils/auth-mechanism-properties.ts deleted file mode 100644 index a5fa2112e5b..00000000000 --- a/packages/connect-form/src/utils/auth-mechanism-properties.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { MongoClientOptions } from 'mongodb'; -import ConnectionStringUrl, { - CommaAndColonSeparatedRecord, -} from 'mongodb-connection-string-url'; - -export interface AuthMechanismProperties { - SERVICE_NAME?: string; - SERVICE_REALM?: string; - CANONICALIZE_HOST_NAME?: boolean; - AWS_SESSION_TOKEN?: string; -} - -export function parseAuthMechanismProperties( - connectionString: ConnectionStringUrl -): CommaAndColonSeparatedRecord { - const searchParams = connectionString.typedSearchParams(); - const authMechanismPropertiesString = searchParams.get( - 'authMechanismProperties' - ); - try { - return new CommaAndColonSeparatedRecord( - authMechanismPropertiesString - ); - } catch (e) { - return new CommaAndColonSeparatedRecord(); - } -} diff --git a/packages/connect-form/src/utils/authentication-handler.ts b/packages/connect-form/src/utils/authentication-handler.ts index bc9d084415c..2be3950b879 100644 --- a/packages/connect-form/src/utils/authentication-handler.ts +++ b/packages/connect-form/src/utils/authentication-handler.ts @@ -3,7 +3,12 @@ import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionOptions } from 'mongodb-data-service'; import type { MongoClientOptions } from 'mongodb'; -import { ConnectionFormError, tryToParseConnectionString } from './validation'; +import { ConnectionFormError } from './validation'; +import { + setConnectionStringPassword, + setConnectionStringUsername, + tryToParseConnectionString, +} from './connection-string-helpers'; export type UpdateAuthMechanismAction = { type: 'update-auth-mechanism'; @@ -89,9 +94,10 @@ export function handleUpdateUsername({ connectionOptions: ConnectionOptions; errors?: ConnectionFormError[]; } { - const updatedConnectionString = connectionStringUrl.clone(); - - updatedConnectionString.username = encodeURIComponent(action.username); + const updatedConnectionString = setConnectionStringUsername( + connectionStringUrl, + action.username + ); const [, parsingError] = tryToParseConnectionString( updatedConnectionString.toString() @@ -131,9 +137,10 @@ export function handleUpdatePassword({ connectionOptions: ConnectionOptions; errors?: ConnectionFormError[]; } { - const updatedConnectionString = connectionStringUrl.clone(); - - updatedConnectionString.password = encodeURIComponent(action.password); + const updatedConnectionString = setConnectionStringPassword( + connectionStringUrl, + action.password + ); const [, parsingError] = tryToParseConnectionString( updatedConnectionString.toString() diff --git a/packages/connect-form/src/utils/connection-string-helpers.spec.ts b/packages/connect-form/src/utils/connection-string-helpers.spec.ts new file mode 100644 index 00000000000..af47fd49410 --- /dev/null +++ b/packages/connect-form/src/utils/connection-string-helpers.spec.ts @@ -0,0 +1,133 @@ +import { expect } from 'chai'; +import ConnectionStringUrl from 'mongodb-connection-string-url'; +import { + getConnectionStringPassword, + getConnectionStringUsername, + parseAuthMechanismProperties, + setConnectionStringPassword, + setConnectionStringUsername, + tryToParseConnectionString, +} from './connection-string-helpers'; + +describe('connection-string-helpers', function () { + describe('#parseAuthMechanismProperties', function () { + it('parses legit authMechanismProperties string', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal('serviceName'); + }); + + it('parses broken authMechanismProperties string as empty', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=broken' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal(undefined); + }); + }); + + describe('#tryToParseConnectionString', function () { + it('should return the connection string when successfully parsed', function () { + const [connectionString] = tryToParseConnectionString( + 'mongodb://outerspace:27099?directConnection=true' + ); + + expect(connectionString.toString()).to.equal( + 'mongodb://outerspace:27099/?directConnection=true' + ); + expect(connectionString.hosts[0]).to.equal('outerspace:27099'); + }); + + it('should return without an error when successfully parsed', function () { + const [connectionString, error] = tryToParseConnectionString( + 'mongodb://outerspace:27099/?directConnection=true' + ); + + expect(connectionString).to.not.equal(undefined); + expect(error).to.equal(undefined); + }); + + it('should return an error when it cannot be parsed', function () { + const [connectionString, error] = tryToParseConnectionString( + 'mangos://pineapple:27099/?directConnection=true' + ); + + expect(connectionString).to.equal(undefined); + expect(error.message).to.equal( + 'Invalid connection string "mangos://pineapple:27099/?directConnection=true"' + ); + }); + }); + + describe('#getConnectionStringUsername', function () { + it('returns a decoded username', function () { + const username = getConnectionStringUsername( + new ConnectionStringUrl( + 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345' + ) + ); + + expect(username).to.equal('C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk'); + }); + }); + + describe('#getConnectionStringPassword', function () { + it('returns a decoded password', function () { + const password = getConnectionStringPassword( + new ConnectionStringUrl( + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345' + ) + ); + + expect(password).to.equal('C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk'); + }); + }); + + describe('#setConnectionStringUsername', function () { + it('sets an encoded username and does not mutate the param', function () { + const connectionString = new ConnectionStringUrl( + 'mongodb://username:password@outerspace:12345' + ); + + expect( + setConnectionStringUsername( + connectionString, + 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' + ).href + ).to.equal( + 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345/' + ); + + expect(connectionString.href).to.equal( + 'mongodb://username:password@outerspace:12345/' + ); + }); + }); + + describe('#setConnectionStringPassword', function () { + it('sets an encoded password and does not mutate the param', function () { + const connectionString = new ConnectionStringUrl( + 'mongodb://username:password@outerspace:12345' + ); + + expect( + setConnectionStringPassword( + connectionString, + 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' + ).href + ).to.equal( + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345/' + ); + + expect(connectionString.href).to.equal( + 'mongodb://username:password@outerspace:12345/' + ); + }); + }); +}); diff --git a/packages/connect-form/src/utils/connection-string-helpers.ts b/packages/connect-form/src/utils/connection-string-helpers.ts new file mode 100644 index 00000000000..3ee1e319a0b --- /dev/null +++ b/packages/connect-form/src/utils/connection-string-helpers.ts @@ -0,0 +1,68 @@ +import type { MongoClientOptions } from 'mongodb'; +import ConnectionStringUrl, { + CommaAndColonSeparatedRecord, +} from 'mongodb-connection-string-url'; + +export interface AuthMechanismProperties { + SERVICE_NAME?: string; + SERVICE_REALM?: string; + CANONICALIZE_HOST_NAME?: boolean; + AWS_SESSION_TOKEN?: string; +} + +export function parseAuthMechanismProperties( + connectionString: ConnectionStringUrl +): CommaAndColonSeparatedRecord { + const searchParams = connectionString.typedSearchParams(); + const authMechanismPropertiesString = searchParams.get( + 'authMechanismProperties' + ); + try { + return new CommaAndColonSeparatedRecord( + authMechanismPropertiesString + ); + } catch (e) { + return new CommaAndColonSeparatedRecord(); + } +} + +export function tryToParseConnectionString( + connectionString: string +): [ConnectionStringUrl | undefined, Error | undefined] { + try { + const connectionStringUrl = new ConnectionStringUrl(connectionString); + return [connectionStringUrl, undefined]; + } catch (err) { + return [undefined, err as Error]; + } +} + +export function getConnectionStringUsername( + connectionStringUrl: ConnectionStringUrl +): string { + return decodeURIComponent(connectionStringUrl.username); +} + +export function getConnectionStringPassword( + connectionStringUrl: ConnectionStringUrl +): string { + return decodeURIComponent(connectionStringUrl.password); +} + +export function setConnectionStringUsername( + connectionStringUrl: ConnectionStringUrl, + username: string +): ConnectionStringUrl { + const updated = connectionStringUrl.clone(); + updated.username = encodeURIComponent(username); + return updated; +} + +export function setConnectionStringPassword( + connectionStringUrl: ConnectionStringUrl, + password: string +): ConnectionStringUrl { + const updated = connectionStringUrl.clone(); + updated.password = encodeURIComponent(password); + return updated; +} diff --git a/packages/connect-form/src/utils/validation.spec.ts b/packages/connect-form/src/utils/validation.spec.ts index 51f88952a8b..95ef2d0970f 100644 --- a/packages/connect-form/src/utils/validation.spec.ts +++ b/packages/connect-form/src/utils/validation.spec.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { - tryToParseConnectionString, validateConnectionOptionsErrors, validateConnectionOptionsWarnings, } from './validation'; @@ -484,37 +483,4 @@ describe('validation', function () { }); }); }); - - describe('#tryToParseConnectionString', function () { - it('should return the connection string when successfully parsed', function () { - const [connectionString] = tryToParseConnectionString( - 'mongodb://outerspace:27099?directConnection=true' - ); - - expect(connectionString.toString()).to.equal( - 'mongodb://outerspace:27099/?directConnection=true' - ); - expect(connectionString.hosts[0]).to.equal('outerspace:27099'); - }); - - it('should return without an error when successfully parsed', function () { - const [connectionString, error] = tryToParseConnectionString( - 'mongodb://outerspace:27099/?directConnection=true' - ); - - expect(connectionString).to.not.equal(undefined); - expect(error).to.equal(undefined); - }); - - it('should return an error when it cannot be parsed', function () { - const [connectionString, error] = tryToParseConnectionString( - 'mangos://pineapple:27099/?directConnection=true' - ); - - expect(connectionString).to.equal(undefined); - expect(error.message).to.equal( - 'Invalid connection string "mangos://pineapple:27099/?directConnection=true"' - ); - }); - }); }); diff --git a/packages/connect-form/src/utils/validation.ts b/packages/connect-form/src/utils/validation.ts index d2dfdcb176b..25898e3e07f 100644 --- a/packages/connect-form/src/utils/validation.ts +++ b/packages/connect-form/src/utils/validation.ts @@ -30,17 +30,6 @@ export interface ConnectionFormWarning { message: string; } -export function tryToParseConnectionString( - connectionString: string -): [ConnectionString | undefined, Error | undefined] { - try { - const connectionStringUrl = new ConnectionString(connectionString); - return [connectionStringUrl, undefined]; - } catch (err) { - return [undefined, err as Error]; - } -} - export function errorMessageByFieldName( errors: ConnectionFormError[], fieldName: FieldName From 34de0247e85be269642c0098cc651d9816148b9f Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 20:35:45 +0100 Subject: [PATCH 7/9] use get by label in tests --- .../authentication-gssapi.spec.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx index c9e7ed0c66a..40fa0ead913 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx @@ -26,7 +26,7 @@ function renderComponent({ ); } -describe('AuthenticationGssapi Component', function () { +describe.only('AuthenticationGssapi Component', function () { let updateConnectionFormFieldSpy: sinon.SinonSpy; beforeEach(function () { updateConnectionFormFieldSpy = sinon.spy(); @@ -39,7 +39,7 @@ describe('AuthenticationGssapi Component', function () { }); expect(updateConnectionFormFieldSpy.callCount).to.equal(0); - fireEvent.change(screen.getAllByRole('textbox')[0], { + fireEvent.change(screen.getByLabelText('Principal'), { target: { value: 'good sandwich' }, }); }); @@ -60,7 +60,7 @@ describe('AuthenticationGssapi Component', function () { }); expect(updateConnectionFormFieldSpy.callCount).to.equal(0); - fireEvent.change(screen.getAllByRole('textbox')[1], { + fireEvent.change(screen.getByLabelText('Service Name'), { target: { value: 'good sandwich' }, }); }); @@ -82,7 +82,7 @@ describe('AuthenticationGssapi Component', function () { }); expect(updateConnectionFormFieldSpy.callCount).to.equal(0); - fireEvent.change(screen.getAllByRole('textbox')[2], { + fireEvent.change(screen.getByLabelText('Service Realm'), { target: { value: 'good sandwich' }, }); }); @@ -102,9 +102,10 @@ describe('AuthenticationGssapi Component', function () { renderComponent({ updateConnectionFormField: updateConnectionFormFieldSpy, }); - expect(updateConnectionFormFieldSpy.callCount).to.equal(0); - fireEvent.click(screen.getByRole('checkbox')); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + const checkbox = screen.getByLabelText('Canonicalize Host Name'); + fireEvent.click(checkbox); }); it('calls to update the form field', function () { From 9295be93b23dd1e8cc4a65cda9a99a5c8d36fc18 Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 20:37:41 +0100 Subject: [PATCH 8/9] unskip tests --- .../authentication-tab/authentication-gssapi.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx index 40fa0ead913..aafcc909271 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx @@ -26,7 +26,7 @@ function renderComponent({ ); } -describe.only('AuthenticationGssapi Component', function () { +describe('AuthenticationGssapi Component', function () { let updateConnectionFormFieldSpy: sinon.SinonSpy; beforeEach(function () { updateConnectionFormFieldSpy = sinon.spy(); From df5e11994787b1bfe8de46ece53980f81bb39471 Mon Sep 17 00:00:00 2001 From: mcasimir Date: Tue, 25 Jan 2022 20:39:23 +0100 Subject: [PATCH 9/9] fix lint and format --- package.json | 1 + .../authentication-tab/authentication-default.tsx | 5 ++++- .../authentication-tab/authentication-gssapi.tsx | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7664c20afad..b3400730077 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "packages-version": "lerna version --allow-branch main --no-push --no-private -m \"chore(release): Bump package versions\"", "release": "npm run release --workspace mongodb-compass --", "reformat": "lerna run reformat --stream --no-bail", + "reformat-changed": "lerna run reformat --stream --no-bail --since origin/HEAD --exclude-dependents", "package-compass": "npm run package-compass --workspace=mongodb-compass --", "prestart": "npm run compile --workspace=@mongodb-js/webpack-config-compass", "start": "npm run start --workspace=mongodb-compass", diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx index d8cd3dd4601..bbe126ae46f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx @@ -18,7 +18,10 @@ import { ConnectionFormError, errorMessageByFieldName, } from '../../../utils/validation'; -import { getConnectionStringPassword, getConnectionStringUsername } from '../../../utils/connection-string-helpers'; +import { + getConnectionStringPassword, + getConnectionStringUsername, +} from '../../../utils/connection-string-helpers'; const authSourceLabelStyles = css({ padding: 0, diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index 2cef77a7a20..873e2a6a902 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -8,7 +8,10 @@ import { ConnectionFormError, errorMessageByFieldName, } from '../../../utils/validation'; -import { getConnectionStringUsername, parseAuthMechanismProperties } from '../../../utils/connection-string-helpers'; +import { + getConnectionStringUsername, + parseAuthMechanismProperties, +} from '../../../utils/connection-string-helpers'; function AuthenticationGSSAPI({ errors,