Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/features/layout/LeftNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const LeftNavBar: FC = () => {
icon={faDatabase}
badge={'alpha'}
badgeColor={'warning'}
requiredRole="CDM_JUPYTERHUB_ADMIN"
requiredRole="BERDL_USER"
/>
</ul>
<ul className={classes.nav_list}>
Expand Down
68 changes: 68 additions & 0 deletions src/features/signup/AccountInformation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,72 @@ describe('AccountInformation', () => {

expect(mockNavigate).toHaveBeenCalledWith('/signup/3');
});

test.each([
['uppercase letters', 'BadUser'],
['a leading digit', '1baduser'],
['a hyphen', 'bad-user'],
['a period', 'bad.user'],
['repeating underscores', 'bad__user'],
['a trailing underscore', 'baduser_'],
])(
'blocks submission and shows format error for username with %s',
async (_label, badName) => {
const store = createTestStore();
store.dispatch(
setLoginData({
creationallowed: true,
expires: 0,
login: [],
provider: 'Google',
create: [
{
provemail: 'test@test.com',
provfullname: 'Test User',
availablename: 'testuser',
id: '123',
provusername: 'testuser',
},
],
})
);
renderWithProviders(<AccountInformation />, { store });

await act(() => {
fireEvent.change(screen.getByRole('textbox', { name: /Full Name/i }), {
target: { value: 'Test User' },
});
});
await act(() => {
fireEvent.change(screen.getByRole('textbox', { name: /Email/i }), {
target: { value: 'test@test.com' },
});
});
await act(() => {
fireEvent.change(
screen.getByRole('textbox', { name: /KBase Username/i }),
{ target: { value: badName } }
);
});
await act(() => {
fireEvent.change(
screen.getByRole('textbox', { name: /Organization/i }),
{ target: { value: 'Test Org' } }
);
});
await act(() => {
fireEvent.change(screen.getByRole('textbox', { name: /Department/i }), {
target: { value: 'Test Dept' },
});
});
await act(() => {
fireEvent.submit(screen.getByTestId('accountinfoform'));
});

expect(
screen.getByText(/may contain only lowercase letters/i)
).toBeInTheDocument();
expect(mockNavigate).not.toHaveBeenCalledWith('/signup/3');
}
);
});
41 changes: 37 additions & 4 deletions src/features/signup/AccountInformation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ export const AccountInformation: FC<{}> = () => {
const [username, setUsername] = useState(account.username ?? '');
const userAvail = loginUsernameSuggest.useQuery(username);
const nameShort = username.length < 3;
const nameAvail =
userAvail.currentData?.availablename === username.toLowerCase();
const nameTooLong = username.length > 100;
// Mirrors backend rules in kbase/auth2 NewUserName: must start with a
// lowercase letter; only [a-z0-9_]; no repeating or trailing underscores.
const nameFormatValid =
/^[a-z][a-z0-9_]*$/.test(username) &&
!username.includes('__') &&
!username.endsWith('_');
const nameAvail = userAvail.currentData?.availablename === username;

const surveyQuestion = 'How did you hear about us? (select all that apply)';
const [optionalText, setOptionalText] = useState<Record<string, string>>({});
Expand Down Expand Up @@ -199,7 +205,11 @@ export const AccountInformation: FC<{}> = () => {
required: true,
onChange: (e) => setUsername(e.currentTarget.value),
validate: () =>
!nameShort && !userAvail.isFetching && nameAvail,
!nameShort &&
!nameTooLong &&
nameFormatValid &&
!userAvail.isFetching &&
nameAvail,
})}
defaultValue={account.username}
helperText={
Expand All @@ -209,6 +219,24 @@ export const AccountInformation: FC<{}> = () => {
Username is too short.
<br />
</span>
) : nameTooLong ? (
<span>
Username must be at most 100 characters.
<br />
</span>
) : !nameFormatValid ? (
<span>
Username may contain only lowercase letters, digits, and
underscores, and must start with a letter. Underscores
cannot repeat or end the username.
{userAvail.currentData?.availablename ? (
<>
{' '}
Suggested: "{userAvail.currentData.availablename}".
</>
) : null}
<br />
</span>
) : !nameAvail && !userAvail.isFetching ? (
<span>
Username is not available. Suggested: "
Expand All @@ -224,7 +252,12 @@ export const AccountInformation: FC<{}> = () => {
</span>
</>
}
error={nameShort || (!userAvail.isFetching && !nameAvail)}
error={
nameShort ||
nameTooLong ||
!nameFormatValid ||
(!userAvail.isFetching && !nameAvail)
}
/>
</FormControl>
<FormControl>
Expand Down
43 changes: 43 additions & 0 deletions src/features/signup/SignupSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createTestStore } from '../../app/store';
import { setLoginData } from './SignupSlice';

const makeLoginData = (
provider: string,
availablename: string
): Parameters<typeof setLoginData>[0] => ({
creationallowed: true,
expires: 0,
login: [],
provider,
create: [
{
provemail: 'jane@example.com',
provfullname: 'Jane Doe',
availablename,
id: '123',
provusername: '0000-0002-1825-0097',
},
],
});

describe('signup setLoginData', () => {
test('pre-fills username from availablename for non-ORCID providers', () => {
const store = createTestStore();
store.dispatch(setLoginData(makeLoginData('Google', 'janedoe')));
expect(store.getState().signup.account.username).toBe('janedoe');
});

test('leaves username blank for ORCID logins to avoid user<N> default', () => {
const store = createTestStore();
store.dispatch(setLoginData(makeLoginData('OrcID', 'user1')));
expect(store.getState().signup.account.username).toBeUndefined();
});

test('still pre-fills display name and email for ORCID', () => {
const store = createTestStore();
store.dispatch(setLoginData(makeLoginData('OrcID', 'user1')));
const account = store.getState().signup.account;
expect(account.display).toBe('Jane Doe');
expect(account.email).toBe('jane@example.com');
});
});
13 changes: 10 additions & 3 deletions src/features/signup/SignupSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@ export const signupSlice = createSlice({
// Set provider creeation data
state.loginData = action.payload;
// Set account defaults from provider
state.account.display = action.payload?.create[0].provfullname;
state.account.email = action.payload?.create[0].provemail;
state.account.username = action.payload?.create[0].availablename;
const detail = action.payload?.create[0];
state.account.display = detail?.provfullname;
state.account.email = detail?.provemail;
// ORCID's provusername is the numeric ORCID iD, which auth2 cannot
// sanitize into a valid username and falls back to user<N>. Leave the
// field blank for ORCID so the user picks their own.
state.account.username =
action.payload?.provider === 'OrcID'
? undefined
: detail?.availablename;
},
setAccount: (
state,
Expand Down
Loading