-
Notifications
You must be signed in to change notification settings - Fork 235
feat(connect-form): add username/password auth options COMPASS-5435 #2707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
adff597
3c0218b
f1b9bd6
8fdc753
67fb7cc
647694e
d84c040
316fb81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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; | ||
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently this action also sets empty values, ie. it would set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On this page the |
||
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> | ||
</> | ||
); | ||
} | ||
|
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.