Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface TabObject {
errors: ConnectionFormError[];
connectionStringUrl: ConnectionStringUrl;
updateConnectionFormField: UpdateConnectionFormField;
connectionOptions?: ConnectionOptions;
connectionOptions: ConnectionOptions;
}>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 AuthenticationDefault from './authentication-default';
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(
<AuthenticationDefault
errors={errors}
connectionStringUrl={connectionStringUrl}
updateConnectionFormField={updateConnectionFormField}
/>
);
}

describe('AuthenticationDefault Component', function () {
let updateConnectionFormFieldSpy: sinon.SinonSpy;
beforeEach(function () {
updateConnectionFormFieldSpy = sinon.spy();
});

describe('when the username 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 password input is changed', function () {
beforeEach(function () {
renderComponent({
updateConnectionFormField: updateConnectionFormFieldSpy,
});
expect(updateConnectionFormFieldSpy.callCount).to.equal(0);

fireEvent.change(screen.getByLabelText('Password'), {
target: { value: '1234' },
});
});

it('calls to update the form field', function () {
expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
type: 'update-password',
password: '1234',
});
});
});

describe('when the auth database input is changed', function () {
beforeEach(function () {
renderComponent({
updateConnectionFormField: updateConnectionFormFieldSpy,
});
expect(updateConnectionFormFieldSpy.callCount).to.equal(0);

fireEvent.change(screen.getAllByRole('textbox')[1], {
target: { value: 'wave' },
});
});

it('calls to update the search param on the connection', function () {
expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
type: 'update-search-param',
currentKey: 'authSource',
value: 'wave',
});
});
});

describe('when the auth database input is changed to an empty value', function () {
beforeEach(function () {
renderComponent({
updateConnectionFormField: updateConnectionFormFieldSpy,
connectionStringUrl: new ConnectionStringUrl(
'mongodb://localhost:27017?authSource=testers'
),
});
expect(updateConnectionFormFieldSpy.callCount).to.equal(0);

fireEvent.change(screen.getAllByRole('textbox')[1], {
target: { value: '' },
});
});

it('calls to delete the search param on the connection', function () {
expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
type: 'delete-search-param',
key: 'authSource',
});
});
});

describe('when a new auth mechanism is clicked', function () {
beforeEach(function () {
renderComponent({
updateConnectionFormField: updateConnectionFormFieldSpy,
});
expect(updateConnectionFormFieldSpy.callCount).to.equal(0);

fireEvent.click(screen.getAllByRole('radio')[2]);
});

it('calls to update the auth mechanism', function () {
expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
type: 'update-search-param',
currentKey: 'authMechanism',
value: 'SCRAM-SHA-256',
});
});
});

it('renders a username error when there is a username error', function () {
renderComponent({
errors: [
{
fieldName: 'username',
message: 'pineapples',
},
],
updateConnectionFormField: updateConnectionFormFieldSpy,
});

expect(screen.getByText('pineapples')).to.be.visible;
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,181 @@
import React from 'react';
import React, { useCallback } from 'react';
import {
Icon,
IconButton,
Label,
RadioBox,
RadioBoxGroup,
TextInput,
css,
spacing,
} from '@mongodb-js/compass-components';
import ConnectionStringUrl from 'mongodb-connection-string-url';
import { AuthMechanism } from 'mongodb';

import { UpdateConnectionFormField } from '../../../hooks/use-connect-form';
import FormFieldContainer from '../../form-field-container';
import { ConnectionFormError } from '../../../utils/validation';

const authSourceLabelStyles = css({
padding: 0,
margin: 0,
flexGrow: 1,
});

const infoButtonStyles = css({
verticalAlign: 'middle',
marginTop: -spacing[1],
});

const defaultAuthMechanismOptions: {
title: string;
value: AuthMechanism;
}[] = [
{
title: 'Default',
value: AuthMechanism.MONGODB_DEFAULT,
},
{
title: 'SCRAM-SHA-1',
value: AuthMechanism.MONGODB_SCRAM_SHA1,
},
{
title: 'SCRAM-SHA-256',
value: AuthMechanism.MONGODB_SCRAM_SHA256,
},
];

function AuthenticationDefault({
errors,
connectionStringUrl,
updateConnectionFormField,
}: {
connectionStringUrl: ConnectionStringUrl;
errors: ConnectionFormError[];
updateConnectionFormField: UpdateConnectionFormField;
}): React.ReactElement {
const password = connectionStringUrl.password;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this relevant here? https://jira.mongodb.org/browse/COMPASS-5097

can we at least add a test or 2 with the weird password there?

Copy link
Member Author

@Anemy Anemy Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do that work when we do that ticket? This work is definitely related, but since we have a ticket for it, that might be a better place to make a pr, test all of the cases and make those changes.
Moved it into this sprint, can pickup after this is merged.

const username = connectionStringUrl.username;

const selectedAuthMechanism =
connectionStringUrl.searchParams.get('authMechanism') ?? '';
const selectedAuthTab =
defaultAuthMechanismOptions.find(
({ value }) => value === selectedAuthMechanism
) ?? defaultAuthMechanismOptions[0];

const onAuthMechanismSelected = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
updateConnectionFormField({
type: 'update-search-param',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this action also sets empty values, ie. it would set &authMechanism=& for a null value, could we also add updatedSearchParams.delete(action.currentKey); in case the value is empty here https://github.com/mongodb-js/compass/blob/main/packages/connect-form/src/hooks/use-connect-form.ts#L386-L393?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see how that helper could be useful here, but I think we want to allow empty values to be set since we use this in the advanced tab to help populate the advanced options. In that case setting something to empty is useful as part of the process of entering those options:
Screen Shot 2022-01-24 at 1 43 41 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that is so useful though, there is no search param that makes sense as empty, and I don't know why someone would prefer to populate the value in the connection string rather than in the form after they selected the key there. I believe is a good feature if we produce a cleaner connection string without random empty search params.

Do you have an alternative to at least delete authMechanism and the other params if they are empty?

Copy link
Member Author

@Anemy Anemy Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this page the authMechanism will always be set since it's a radio with 3 options and unsetting isn't one.
Yup we can use the delete-search-param action.
I see what you mean. I'm cool with either in that case, could be nice to have that logic in the update search param handler, but we'll have to update the uri options component to keep a bit of state. Not sure this is the pr for that change.

currentKey: 'authMechanism',
value: event.target.value,
});
},
[updateConnectionFormField]
);

const usernameError = errors?.find((error) => error.fieldName === 'username');

function AuthenticationDefault(): React.ReactElement {
return (
<>
<p>Username and Password</p>
<FormFieldContainer>
<TextInput
onChange={({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
updateConnectionFormField({
type: 'update-username',
username: value,
});
}}
label="Username"
errorMessage={usernameError?.message}
state={usernameError ? 'error' : undefined}
value={username || ''}
/>
</FormFieldContainer>
<FormFieldContainer>
<TextInput
onChange={({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
updateConnectionFormField({
type: 'update-password',
password: value,
});
}}
label="Password"
type="password"
value={password || ''}
/>
</FormFieldContainer>
<FormFieldContainer>
<Label
className={authSourceLabelStyles}
htmlFor="authSourceInput"
id="authSourceLabel"
>
Authentication Database
<IconButton
className={infoButtonStyles}
aria-label="Authentication Database Documentation"
href="https://docs.mongodb.com/manual/reference/connection-string/#mongodb-urioption-urioption.authSource"
target="_blank"
>
<Icon glyph="InfoWithCircle" size="small" />
</IconButton>
</Label>

<TextInput
onChange={({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
if (value === '') {
updateConnectionFormField({
type: 'delete-search-param',
key: 'authSource',
});
return;
}
updateConnectionFormField({
type: 'update-search-param',
currentKey: 'authSource',
value,
});
}}
id="authSourceInput"
aria-labelledby="authSourceLabel"
value={connectionStringUrl.searchParams.get('authSource') ?? ''}
optional
/>
</FormFieldContainer>
<FormFieldContainer>
<Label htmlFor="authentication-mechanism-radio-box-group">
Authentication Mechanism
</Label>
<RadioBoxGroup
onChange={onAuthMechanismSelected}
id="authentication-mechanism-radio-box-group"
size="default"
value={selectedAuthTab.value}
>
{defaultAuthMechanismOptions.map(({ title, value }) => {
return (
<RadioBox
data-testid={`${value}-tab-button`}
checked={selectedAuthTab.value === value}
value={value}
key={value}
size="default"
>
{title}
</RadioBox>
);
})}
</RadioBoxGroup>
</FormFieldContainer>
</>
);
}
Expand Down
Loading