Merge pull request #2106 from automatisch/AUT-1265

feat: refactor update-current-user mutation with the REST API endpoint
This commit is contained in:
Ali BARIN
2024-09-30 10:11:22 +02:00
committed by GitHub
11 changed files with 290 additions and 93 deletions

View File

@@ -1,6 +1,5 @@
// Converted mutations
import verifyConnection from './mutations/verify-connection.js';
import updateCurrentUser from './mutations/update-current-user.js';
import generateAuthUrl from './mutations/generate-auth-url.js';
import resetConnection from './mutations/reset-connection.js';
import updateConnection from './mutations/update-connection.js';
@@ -9,7 +8,6 @@ const mutationResolvers = {
generateAuthUrl,
resetConnection,
updateConnection,
updateCurrentUser,
verifyConnection,
};

View File

@@ -1,11 +0,0 @@
const updateCurrentUser = async (_parent, params, context) => {
const user = await context.currentUser.$query().patchAndFetch({
email: params.input.email,
password: params.input.password,
fullName: params.input.fullName,
});
return user;
};
export default updateCurrentUser;

View File

@@ -5,7 +5,6 @@ type Mutation {
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
resetConnection(input: ResetConnectionInput): Connection
updateConnection(input: UpdateConnectionInput): Connection
updateCurrentUser(input: UpdateCurrentUserInput): User
verifyConnection(input: VerifyConnectionInput): Connection
}
@@ -223,12 +222,6 @@ input UserRoleInput {
id: String
}
input UpdateCurrentUserInput {
email: String
password: String
fullName: String
}
"""
The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
"""

View File

@@ -6,9 +6,11 @@ export class MyProfilePage extends AuthenticatedPage {
this.fullName = this.page.locator('[name="fullName"]');
this.email = this.page.locator('[name="email"]');
this.currentPassword = this.page.locator('[name="currentPassword"]');
this.newPassword = this.page.locator('[name="password"]');
this.passwordConfirmation = this.page.locator('[name="confirmPassword"]');
this.updateProfileButton = this.page.getByTestId('update-profile-button');
this.updatePasswordButton = this.page.getByTestId('update-password-button');
this.settingsMenuItem = this.page.getByRole('menuitem', {
name: 'Settings',
});

View File

@@ -1,17 +1,19 @@
const { publicTest, expect } = require('../../fixtures/index');
const { AdminUsersPage } = require('../../fixtures/admin/users-page');
const { MyProfilePage } = require('../../fixtures/my-profile-page');
const { LoginPage } = require('../../fixtures/login-page');
publicTest.describe('My Profile', () => {
publicTest(
'user should be able to change own data',
let testUser;
publicTest.beforeEach(
async ({ acceptInvitationPage, adminCreateUserPage, loginPage, page }) => {
let acceptInvitationLink;
adminCreateUserPage.seed(
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
);
const testUser = adminCreateUserPage.generateUser();
testUser = adminCreateUserPage.generateUser();
const adminUsersPage = new AdminUsersPage(page);
const myProfilePage = new MyProfilePage(page);
@@ -49,27 +51,76 @@ publicTest.describe('My Profile', () => {
await publicTest.step('accept invitation', async () => {
await page.goto(acceptInvitationLink);
await acceptInvitationPage.acceptInvitation(process.env.LOGIN_PASSWORD);
await acceptInvitationPage.acceptInvitation(LoginPage.defaultPassword);
});
await publicTest.step('login as new Admin', async () => {
await loginPage.login(testUser.email, process.env.LOGIN_PASSWORD);
await loginPage.login(testUser.email, LoginPage.defaultPassword);
await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows');
});
}
);
await publicTest.step('change own data', async () => {
publicTest('user should be able to change own data', async ({ page }) => {
const myProfilePage = new MyProfilePage(page);
await publicTest.step('change own data', async () => {
await myProfilePage.navigateTo();
await myProfilePage.fullName.fill('abecadło');
await myProfilePage.email.fill('a' + testUser.email);
await myProfilePage.updateProfileButton.click();
});
await publicTest.step('verify changed data', async () => {
await expect(myProfilePage.fullName).toHaveValue('abecadło');
await expect(myProfilePage.email).toHaveValue('a' + testUser.email);
await page.reload();
await expect(myProfilePage.fullName).toHaveValue('abecadło');
await expect(myProfilePage.email).toHaveValue('a' + testUser.email);
});
});
publicTest(
'user should not be able to change email to already existing one',
async ({ page }) => {
const myProfilePage = new MyProfilePage(page);
await publicTest.step('change email to existing one', async () => {
await myProfilePage.navigateTo();
await myProfilePage.fullName.fill('abecadło');
await myProfilePage.email.fill('a' + testUser.email);
await myProfilePage.email.fill(LoginPage.defaultEmail);
await myProfilePage.updateProfileButton.click();
});
await publicTest.step('verify error message', async () => {
const snackbar = await myProfilePage.getSnackbarData(
'snackbar-update-profile-settings-error'
);
await expect(snackbar.variant).toBe('error');
});
}
);
publicTest(
'user should be able to change own password',
async ({ loginPage, page }) => {
const myProfilePage = new MyProfilePage(page);
await publicTest.step('change own password', async () => {
await myProfilePage.navigateTo();
await myProfilePage.currentPassword.fill(LoginPage.defaultPassword);
await myProfilePage.newPassword.fill(
process.env.LOGIN_PASSWORD + process.env.LOGIN_PASSWORD
LoginPage.defaultPassword + LoginPage.defaultPassword
);
await myProfilePage.passwordConfirmation.fill(
process.env.LOGIN_PASSWORD + process.env.LOGIN_PASSWORD
LoginPage.defaultPassword + LoginPage.defaultPassword
);
await myProfilePage.updateProfileButton.click();
await myProfilePage.updatePasswordButton.click();
});
await publicTest.step('logout', async () => {
@@ -78,17 +129,58 @@ publicTest.describe('My Profile', () => {
await publicTest.step('login with new credentials', async () => {
await loginPage.login(
'a' + testUser.email,
process.env.LOGIN_PASSWORD + process.env.LOGIN_PASSWORD
testUser.email,
LoginPage.defaultPassword + LoginPage.defaultPassword
);
await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows');
});
await publicTest.step('verify changed data', async () => {
await publicTest.step('verify if user is the same', async () => {
await myProfilePage.navigateTo();
await expect(myProfilePage.fullName).toHaveValue('abecadło');
await expect(myProfilePage.email).toHaveValue('a' + testUser.email);
await expect(myProfilePage.email).toHaveValue(testUser.email);
});
}
);
publicTest(
'user should not be able to change own password if current one is incorrect',
async ({ loginPage, page }) => {
const myProfilePage = new MyProfilePage(page);
await publicTest.step('change own password', async () => {
await myProfilePage.navigateTo();
await myProfilePage.currentPassword.fill('wrongpassword');
await myProfilePage.newPassword.fill(
LoginPage.defaultPassword + LoginPage.defaultPassword
);
await myProfilePage.passwordConfirmation.fill(
LoginPage.defaultPassword + LoginPage.defaultPassword
);
await myProfilePage.updatePasswordButton.click();
});
await publicTest.step('verify error message', async () => {
const snackbar = await myProfilePage.getSnackbarData(
'snackbar-update-password-error'
);
await expect(snackbar.variant).toBe('error');
});
await publicTest.step('logout', async () => {
await myProfilePage.logout();
});
await publicTest.step('login with old credentials', async () => {
await loginPage.login(testUser.email, LoginPage.defaultPassword);
await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows');
});
await publicTest.step('verify if user is the same', async () => {
await myProfilePage.navigateTo();
await expect(myProfilePage.email).toHaveValue(testUser.email);
});
}
);

View File

@@ -86,7 +86,6 @@ export default function ResetPasswordForm() {
: ''
}
/>
<TextField
label={formatMessage(
'acceptInvitationForm.confirmPasswordFieldLabel',
@@ -111,7 +110,7 @@ export default function ResetPasswordForm() {
{acceptInvitation.isError && (
<Alert
data-test='accept-invitation-form-error'
data-test="accept-invitation-form-error"
severity="error"
sx={{ mt: 1, fontWeight: 500 }}
>

View File

@@ -1,10 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_CURRENT_USER = gql`
mutation UpdateCurrentUser($input: UpdateCurrentUserInput) {
updateCurrentUser(input: $input) {
id
fullName
email
}
}
`;

View File

@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useUpdateCurrentUser(userId) {
const queryClient = useQueryClient();
const query = useMutation({
mutationFn: async (payload) => {
const { data } = await api.patch(`/v1/users/${userId}`, payload);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users', 'me'] });
},
});
return query;
}

View File

@@ -0,0 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useUpdateCurrentUserPassword(userId) {
const query = useMutation({
mutationFn: async (payload) => {
const { data } = await api.patch(`/v1/users/${userId}/password`, payload);
return data;
},
});
return query;
}

View File

@@ -102,7 +102,12 @@
"profileSettings.fullName": "Full name",
"profileSettings.email": "Email",
"profileSettings.updateProfile": "Update your profile",
"profileSettings.updatePassword": "Update your password",
"profileSettings.updatedProfile": "Your profile has been updated.",
"profileSettings.updatedPassword": "Your password has been updated.",
"profileSettings.updateProfileError": "Something went wrong while updating your profile.",
"profileSettings.updatePasswordError": "Something went wrong while updating your password.",
"profileSettings.currentPassword": "Current password",
"profileSettings.newPassword": "New password",
"profileSettings.confirmNewPassword": "Confirm new password",
"profileSettings.deleteMyAccount": "Delete my account",

View File

@@ -1,4 +1,3 @@
import { useMutation } from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
@@ -15,19 +14,26 @@ import DeleteAccountDialog from 'components/DeleteAccountDialog/index.ee';
import Form from 'components/Form';
import PageTitle from 'components/PageTitle';
import TextField from 'components/TextField';
import { UPDATE_CURRENT_USER } from 'graphql/mutations/update-current-user';
import useCurrentUser from 'hooks/useCurrentUser';
import useFormatMessage from 'hooks/useFormatMessage';
import { useQueryClient } from '@tanstack/react-query';
import useUpdateCurrentUser from 'hooks/useUpdateCurrentUser';
import useUpdateCurrentUserPassword from 'hooks/useUpdateCurrentUserPassword';
const validationSchema = yup
const validationSchemaProfile = yup
.object({
fullName: yup.string().required(),
email: yup.string().email().required(),
password: yup.string(),
fullName: yup.string().required('Full name is required'),
email: yup.string().email().required('Email is required'),
})
.required();
const validationSchemaPassword = yup
.object({
currentPassword: yup.string().required('Current password is required'),
password: yup.string().required('New password is required'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match'),
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
})
.required();
@@ -37,6 +43,24 @@ const StyledForm = styled(Form)`
flex-direction: column;
`;
const getErrorMessage = (error) => {
const errors = error?.response?.data?.errors || {};
const errorMessages = Object.entries(errors)
.map(([key, messages]) => {
if (Array.isArray(messages) && messages.length) {
return `${key} ${messages.join(', ')}`;
}
if (typeof messages === 'string') {
return `${key} ${messages}`;
}
return '';
})
.filter((message) => !!message)
.join(' ');
return errorMessages;
};
function ProfileSettings() {
const [showDeleteAccountConfirmation, setShowDeleteAccountConfirmation] =
React.useState(false);
@@ -44,42 +68,65 @@ function ProfileSettings() {
const { data } = useCurrentUser();
const currentUser = data?.data;
const formatMessage = useFormatMessage();
const [updateCurrentUser] = useMutation(UPDATE_CURRENT_USER);
const queryClient = useQueryClient();
const { mutateAsync: updateCurrentUser } = useUpdateCurrentUser(
currentUser?.id,
);
const { mutateAsync: updateCurrentUserPassword } =
useUpdateCurrentUserPassword(currentUser?.id);
const handleProfileSettingsUpdate = async (data) => {
const { fullName, password, email } = data;
const mutationInput = {
fullName,
email,
};
try {
const { fullName, email } = data;
if (password) {
mutationInput.password = password;
}
await updateCurrentUser({ fullName, email });
await updateCurrentUser({
variables: {
input: mutationInput,
},
optimisticResponse: {
updateCurrentUser: {
__typename: 'User',
id: currentUser.id,
fullName,
email,
enqueueSnackbar(formatMessage('profileSettings.updatedProfile'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-profile-settings-success',
},
},
});
});
} catch (error) {
enqueueSnackbar(
getErrorMessage(error) ||
formatMessage('profileSettings.updateProfileError'),
{
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-update-profile-settings-error',
},
},
);
}
};
await queryClient.invalidateQueries({ queryKey: ['users', 'me'] });
const handlePasswordUpdate = async (data) => {
try {
const { password, currentPassword } = data;
enqueueSnackbar(formatMessage('profileSettings.updatedProfile'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-profile-settings-success',
},
});
await updateCurrentUserPassword({
currentPassword,
password,
});
enqueueSnackbar(formatMessage('profileSettings.updatedPassword'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-password-success',
},
});
} catch (error) {
enqueueSnackbar(
getErrorMessage(error) ||
formatMessage('profileSettings.updatePasswordError'),
{
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-update-password-error',
},
},
);
}
};
return (
@@ -93,11 +140,9 @@ function ProfileSettings() {
<StyledForm
defaultValues={{
...currentUser,
password: '',
confirmPassword: '',
}}
onSubmit={handleProfileSettingsUpdate}
resolver={yupResolver(validationSchema)}
resolver={yupResolver(validationSchemaProfile)}
mode="onChange"
sx={{ mb: 2 }}
render={({
@@ -117,8 +162,8 @@ function ProfileSettings() {
margin="dense"
error={touchedFields.fullName && !!errors?.fullName}
helperText={errors?.fullName?.message || ' '}
required
/>
<TextField
fullWidth
name="email"
@@ -126,6 +171,57 @@ function ProfileSettings() {
margin="dense"
error={touchedFields.email && !!errors?.email}
helperText={errors?.email?.message || ' '}
required
/>
<Button
variant="contained"
type="submit"
disabled={!isDirty || !isValid || isSubmitting}
data-test="update-profile-button"
>
{formatMessage('profileSettings.updateProfile')}
</Button>
</>
)}
/>
</Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 3 }}>
<StyledForm
defaultValues={{
currentPassword: '',
password: '',
confirmPassword: '',
}}
onSubmit={handlePasswordUpdate}
resolver={yupResolver(validationSchemaPassword)}
mode="onChange"
sx={{ mb: 2 }}
render={({
formState: {
errors,
touchedFields,
isDirty,
isValid,
isSubmitting,
},
}) => (
<>
<TextField
fullWidth
name="currentPassword"
label={formatMessage('profileSettings.currentPassword')}
margin="dense"
type="password"
error={
touchedFields.currentPassword && !!errors?.currentPassword
}
helperText={
(touchedFields.currentPassword &&
errors?.currentPassword?.message) ||
' '
}
required
/>
<TextField
@@ -138,8 +234,8 @@ function ProfileSettings() {
helperText={
(touchedFields.password && errors?.password?.message) || ' '
}
required
/>
<TextField
fullWidth
name="confirmPassword"
@@ -154,15 +250,15 @@ function ProfileSettings() {
errors?.confirmPassword?.message) ||
' '
}
required
/>
<Button
variant="contained"
type="submit"
disabled={!isDirty || !isValid || isSubmitting}
data-test="update-profile-button"
data-test="update-password-button"
>
{formatMessage('profileSettings.updateProfile')}
{formatMessage('profileSettings.updatePassword')}
</Button>
</>
)}