mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added tests for user management (#17128)
refs https://github.com/TryGhost/Team/issues/3349
This commit is contained in:
parent
0281a30fb4
commit
9e325d6b38
17 changed files with 1042 additions and 29 deletions
|
@ -9,6 +9,7 @@ interface ListItemProps {
|
|||
hideActions?: boolean;
|
||||
avatar?: React.ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* Hidden for the last item in the list
|
||||
|
@ -19,7 +20,7 @@ interface ListItemProps {
|
|||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActions, avatar, className, separator, bgOnHover = true, onClick}) => {
|
||||
const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActions, avatar, className, testId, separator, bgOnHover = true, onClick}) => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
onClick?.(e);
|
||||
};
|
||||
|
@ -33,7 +34,7 @@ const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActio
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={listItemClasses}>
|
||||
<div className={listItemClasses} data-testid={testId}>
|
||||
<div className={`flex grow items-center gap-3 ${onClick && 'cursor-pointer'}`} onClick={handleClick}>
|
||||
{avatar && avatar}
|
||||
<div className={`flex grow flex-col py-3 pr-6`} id={id}>
|
||||
|
@ -50,4 +51,4 @@ const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActio
|
|||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
export default ListItem;
|
||||
|
|
|
@ -49,7 +49,7 @@ const Toast: React.FC<ToastProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={classNames} data-testid='toast'>
|
||||
<div className='flex items-start gap-3'>
|
||||
{props?.icon && (typeof props.icon === 'string' ?
|
||||
<div className='mt-0.5'><Icon className='grow' colorClass={props.type === 'success' ? 'text-green' : 'text-white'} name={props.icon} size='sm' /></div> : props.icon)}
|
||||
|
@ -93,4 +93,4 @@ export const showToast = ({
|
|||
...options
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||
width: (unstyled ? '' : width),
|
||||
height: (unstyled ? '' : height)
|
||||
}}>
|
||||
<img alt='' className={imageClassName} src={imageURL} style={{
|
||||
<img alt='' className={imageClassName} id={id} src={imageURL} style={{
|
||||
width: (unstyled ? '' : width || '100%'),
|
||||
height: (unstyled ? '' : height || 'auto')
|
||||
}} />
|
||||
|
@ -123,4 +123,4 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
export default ImageUpload;
|
||||
|
|
|
@ -37,6 +37,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|||
okColor={okColor}
|
||||
okLabel={taskState === 'running' ? okRunningLabel : okLabel}
|
||||
size={540}
|
||||
testId='confirmation-modal'
|
||||
title={title}
|
||||
onCancel={onCancel}
|
||||
onOk={async () => {
|
||||
|
@ -52,4 +53,4 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default NiceModal.create(ConfirmationModal);
|
||||
export default NiceModal.create(ConfirmationModal);
|
||||
|
|
|
@ -119,6 +119,7 @@ const InviteUserModal = NiceModal.create(() => {
|
|||
cancelLabel=''
|
||||
okLabel={okLabel}
|
||||
size={540}
|
||||
testId='invite-user-modal'
|
||||
title='Invite a new staff user'
|
||||
onOk={handleSendInvitation}
|
||||
>
|
||||
|
|
|
@ -328,7 +328,7 @@ const Password: React.FC<UserDetailProps> = ({user}) => {
|
|||
inputRef={newPasswordRef}
|
||||
title="New password"
|
||||
type="password"
|
||||
value=''
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
}}
|
||||
|
@ -339,7 +339,7 @@ const Password: React.FC<UserDetailProps> = ({user}) => {
|
|||
inputRef={confirmNewPasswordRef}
|
||||
title="Verify password"
|
||||
type="password"
|
||||
value=''
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => {
|
||||
setConfirmNewPassword(e.target.value);
|
||||
}}
|
||||
|
@ -399,9 +399,10 @@ interface UserDetailModalProps {
|
|||
}
|
||||
|
||||
const UserMenuTrigger = () => (
|
||||
<div className='flex h-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] px-3 opacity-80 hover:opacity-100'>
|
||||
<button className='flex h-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] px-3 opacity-80 hover:opacity-100' type='button'>
|
||||
<Icon colorClass='text-white' name='ellipsis' size='md' />
|
||||
</div>
|
||||
<span className='sr-only'>Actions</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||
|
@ -486,7 +487,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Transfer Ownership',
|
||||
prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.',
|
||||
okLabel: 'Yep — I\'m sure',
|
||||
okLabel: 'Yep — I\'m sure',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
const res = await api.users.makeOwner(user.id);
|
||||
|
@ -597,6 +598,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
okLabel={okLabel}
|
||||
size='lg'
|
||||
stickyFooter={true}
|
||||
testId='user-detail-modal'
|
||||
onOk={async () => {
|
||||
setSaveState('saving');
|
||||
if (!validator.isEmail(userData.email)) {
|
||||
|
|
|
@ -41,10 +41,10 @@ const Owner: React.FC<OwnerProps> = ({user, updateUser}) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='group flex gap-3 hover:cursor-pointer' onClick={showDetailModal}>
|
||||
<div className='group flex gap-3 hover:cursor-pointer' data-testid='owner-user' onClick={showDetailModal}>
|
||||
<Avatar bgColor={generateAvatarColor((user.name ? user.name : user.email))} image={user.profile_image} label={getInitials(user.name)} labelColor='white' size='lg' />
|
||||
<div className='flex flex-col'>
|
||||
<span>{user.name} — <strong>Owner</strong> <span className='invisible ml-2 inline-block text-sm font-bold text-green group-hover:visible'>Edit</span></span>
|
||||
<span>{user.name} — <strong>Owner</strong> <button className='invisible ml-2 inline-block text-sm font-bold text-green group-hover:visible' type='button'>Edit</button></span>
|
||||
<span className='text-xs text-grey-700'>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -81,6 +81,7 @@ const UsersList: React.FC<UsersListProps> = ({users, updateUser}) => {
|
|||
hideActions={true}
|
||||
id={`list-item-${user.id}`}
|
||||
separator={false}
|
||||
testId='user-list-item'
|
||||
title={title}
|
||||
onClick={() => showDetailModal(user)} />
|
||||
);
|
||||
|
@ -115,7 +116,7 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => {
|
|||
setInvites(res.invites);
|
||||
setRevokeState('');
|
||||
showToast({
|
||||
message: `Invitation revoked(${invite.email})`,
|
||||
message: `Invitation revoked (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
}}
|
||||
|
@ -136,7 +137,7 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => {
|
|||
setInvites(res.invites);
|
||||
setResendState('');
|
||||
showToast({
|
||||
message: `Invitation resent!(${invite.email})`,
|
||||
message: `Invitation resent! (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
}}
|
||||
|
@ -167,6 +168,7 @@ const InvitesUserList: React.FC<InviteListProps> = ({users}) => {
|
|||
hideActions={true}
|
||||
id={`list-item-${user.id}`}
|
||||
separator={false}
|
||||
testId='user-invite'
|
||||
title={user.email}
|
||||
onClick={() => {
|
||||
// do nothing
|
||||
|
@ -234,6 +236,7 @@ const Users: React.FC = () => {
|
|||
customButtons={buttons}
|
||||
navid='users'
|
||||
searchKeywords={['users', 'permissions', 'roles', 'staff']}
|
||||
testId='users'
|
||||
title='Users and permissions'
|
||||
>
|
||||
<Owner updateUser={updateUser} user={ownerUser} />
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface RolesResponseType {
|
|||
export interface UserInvite {
|
||||
created_at: string;
|
||||
email: string;
|
||||
expires: string;
|
||||
expires: number;
|
||||
id: string;
|
||||
role_id: string;
|
||||
role?: string;
|
||||
|
@ -80,7 +80,7 @@ export interface SiteResponseType {
|
|||
export interface ImagesResponseType {
|
||||
images: {
|
||||
url: string;
|
||||
ref: string;
|
||||
ref: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
|
191
apps/admin-x-settings/test/e2e/general/users/actions.test.ts
Normal file
191
apps/admin-x-settings/test/e2e/general/users/actions.test.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
|
||||
test.describe('User actions', async () => {
|
||||
test('Supports suspending a user', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
users: {
|
||||
edit: {
|
||||
users: [{
|
||||
...responseFixtures.users.users.find(user => user.email === 'author@test.com')!,
|
||||
status: 'inactive'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Authors'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await modal.getByRole('button', {name: 'Actions'}).click();
|
||||
await modal.getByRole('button', {name: 'Suspend user'}).click();
|
||||
|
||||
const confirmation = page.getByTestId('confirmation-modal');
|
||||
await confirmation.getByRole('button', {name: 'Suspend'}).click();
|
||||
|
||||
await expect(modal).toHaveText(/Suspended/);
|
||||
|
||||
expect(lastApiRequests.users.edit.body).toMatchObject({
|
||||
users: [{
|
||||
email: 'author@test.com',
|
||||
status: 'inactive'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports un-suspending a user', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
users: {
|
||||
browse: {
|
||||
users: [
|
||||
...responseFixtures.users.users.filter(user => user.email !== 'author@test.com'),
|
||||
{
|
||||
...responseFixtures.users.users.find(user => user.email === 'author@test.com')!,
|
||||
status: 'inactive'
|
||||
}
|
||||
]
|
||||
},
|
||||
edit: {
|
||||
users: [{
|
||||
...responseFixtures.users.users.find(user => user.email === 'author@test.com')!,
|
||||
status: 'active'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Authors'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await expect(modal).toHaveText(/Suspended/);
|
||||
|
||||
await modal.getByRole('button', {name: 'Actions'}).click();
|
||||
await modal.getByRole('button', {name: 'Un-suspend user'}).click();
|
||||
|
||||
const confirmation = page.getByTestId('confirmation-modal');
|
||||
await confirmation.getByRole('button', {name: 'Un-suspend'}).click();
|
||||
|
||||
await expect(modal).not.toHaveText(/Suspended/);
|
||||
|
||||
expect(lastApiRequests.users.edit.body).toMatchObject({
|
||||
users: [{
|
||||
email: 'author@test.com',
|
||||
status: 'active'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports deleting a user', async ({page}) => {
|
||||
const authorUser = responseFixtures.users.users.find(user => user.email === 'author@test.com')!;
|
||||
|
||||
const lastApiRequests = await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Authors'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await modal.getByRole('button', {name: 'Actions'}).click();
|
||||
await modal.getByRole('button', {name: 'Delete user'}).click();
|
||||
|
||||
const confirmation = page.getByTestId('confirmation-modal');
|
||||
await confirmation.getByRole('button', {name: 'Delete user'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/User deleted/);
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveCount(0);
|
||||
|
||||
expect(lastApiRequests.users.delete.url).toMatch(new RegExp(`/users/${authorUser.id}`));
|
||||
});
|
||||
|
||||
test('Supports transferring ownership to an administrator', async ({page}) => {
|
||||
const administrator = responseFixtures.users.users.find(user => user.email === 'administrator@test.com')!;
|
||||
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
users: {
|
||||
makeOwner: {
|
||||
users: [
|
||||
...responseFixtures.users.users.filter(user => user.email !== 'administrator@test.com' && user.email !== 'owner@test.com'),
|
||||
{
|
||||
...administrator,
|
||||
roles: [responseFixtures.roles.roles.find(role => role.name === 'Owner')!]
|
||||
},
|
||||
{
|
||||
...responseFixtures.users.users.find(user => user.email === 'owner@test.com')!,
|
||||
roles: [responseFixtures.roles.roles.find(role => role.name === 'Administrator')!]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
// Can't transfer to a role other than administrator
|
||||
|
||||
await section.getByRole('tab', {name: 'Editors'}).click();
|
||||
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await modal.getByRole('button', {name: 'Actions'}).click();
|
||||
await expect(modal.getByRole('button', {name: 'Make owner'})).toHaveCount(0);
|
||||
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
|
||||
// Can transfer to an administrator
|
||||
|
||||
await section.getByRole('tab', {name: 'Administrators'}).click();
|
||||
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await modal.getByRole('button', {name: 'Actions'}).click();
|
||||
await modal.getByRole('button', {name: 'Make owner'}).click();
|
||||
|
||||
const confirmation = page.getByTestId('confirmation-modal');
|
||||
await confirmation.getByRole('button', {name: 'Yep — I\'m sure'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/Ownership transferred/);
|
||||
|
||||
await expect(section.getByTestId('owner-user')).toHaveText(/administrator@test\.com/);
|
||||
|
||||
expect(lastApiRequests.users.makeOwner.body).toMatchObject({
|
||||
owner: [{
|
||||
id: administrator.id
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
108
apps/admin-x-settings/test/e2e/general/users/invite.test.ts
Normal file
108
apps/admin-x-settings/test/e2e/general/users/invite.test.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
|
||||
test.describe('User invitations', async () => {
|
||||
test('Supports inviting a user', async ({page}) => {
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 1);
|
||||
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
invites: {
|
||||
add: {
|
||||
invites: [
|
||||
{
|
||||
id: 'new-invite-id',
|
||||
role_id: '645453f3d254799990dd0e18',
|
||||
status: 'sent',
|
||||
email: 'newuser@test.com',
|
||||
expires: futureDate.getTime(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
|
||||
await section.getByRole('button', {name: 'Invite users'}).click();
|
||||
|
||||
const modal = page.getByTestId('invite-user-modal');
|
||||
await modal.getByLabel('Email address').fill('newuser@test.com');
|
||||
await modal.locator('input[value=author]').check();
|
||||
await modal.getByRole('button', {name: 'Send invitation now'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/Invitation successfully sent to newuser@test\.com/);
|
||||
|
||||
// Currently clicking the backdrop is the only way to close this modal
|
||||
await page.locator('#modal-backdrop').click({position: {x: 0, y: 0}});
|
||||
|
||||
await section.getByRole('tab', {name: 'Invited'}).click();
|
||||
|
||||
const listItem = section.getByTestId('user-invite').last();
|
||||
|
||||
await expect(listItem.getByText('newuser@test.com')).toBeVisible();
|
||||
await expect(listItem.getByText('Author')).toBeVisible();
|
||||
|
||||
expect(lastApiRequests.invites.add.body).toEqual({
|
||||
invites: [{
|
||||
email: 'newuser@test.com',
|
||||
expires: null,
|
||||
role_id: '645453f3d254799990dd0e18',
|
||||
status: null,
|
||||
token: null
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports resending invitations', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
await section.getByRole('tab', {name: 'Invited'}).click();
|
||||
|
||||
const listItem = section.getByTestId('user-invite');
|
||||
await listItem.hover();
|
||||
|
||||
await listItem.getByRole('button', {name: 'Resend'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/Invitation resent! \(invitee@test\.com\)/);
|
||||
|
||||
// Resending works by deleting and re-adding the invite
|
||||
|
||||
expect(lastApiRequests.invites.delete.url).toMatch(new RegExp(`/invites/${responseFixtures.invites.invites[0].id}`));
|
||||
|
||||
expect(lastApiRequests.invites.add.body).toEqual({
|
||||
invites: [{
|
||||
email: 'invitee@test.com',
|
||||
expires: null,
|
||||
role_id: '645453f3d254799990dd0e18',
|
||||
status: null,
|
||||
token: null
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports revoking invitations', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
await section.getByRole('tab', {name: 'Invited'}).click();
|
||||
|
||||
const listItem = section.getByTestId('user-invite');
|
||||
await listItem.hover();
|
||||
|
||||
await listItem.getByRole('button', {name: 'Revoke'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/Invitation revoked \(invitee@test\.com\)/);
|
||||
|
||||
expect(lastApiRequests.invites.delete.url).toMatch(new RegExp(`/invites/${responseFixtures.invites.invites[0].id}`));
|
||||
});
|
||||
});
|
152
apps/admin-x-settings/test/e2e/general/users/profile.test.ts
Normal file
152
apps/admin-x-settings/test/e2e/general/users/profile.test.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
|
||||
test.describe('User profile', async () => {
|
||||
test('Supports editing user profiles', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
users: {
|
||||
edit: {
|
||||
users: [{
|
||||
...responseFixtures.users.users.find(user => user.email === 'administrator@test.com')!,
|
||||
email: 'newadmin@test.com',
|
||||
name: 'New Admin'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Administrators'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await modal.getByLabel('Full name').fill('New Admin');
|
||||
await modal.getByLabel('Email').fill('newadmin@test.com');
|
||||
await modal.getByLabel('Slug').fill('newadmin');
|
||||
await modal.getByLabel('Location').fill('some location');
|
||||
await modal.getByLabel('Website').fill('some site');
|
||||
await modal.getByLabel('Facebook profile').fill('some fb');
|
||||
await modal.getByLabel('Twitter profile').fill('some tw');
|
||||
await modal.getByLabel('Bio').fill('some bio');
|
||||
|
||||
await modal.getByLabel(/New paid members/).uncheck();
|
||||
await modal.getByLabel(/Paid member cancellations/).check();
|
||||
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
|
||||
await expect(listItem.getByText('New Admin')).toBeVisible();
|
||||
await expect(listItem.getByText('newadmin@test.com')).toBeVisible();
|
||||
|
||||
expect(lastApiRequests.users.edit.body).toMatchObject({
|
||||
users: [{
|
||||
email: 'newadmin@test.com',
|
||||
name: 'New Admin',
|
||||
slug: 'newadmin',
|
||||
location: 'some location',
|
||||
website: 'some site',
|
||||
facebook: 'some fb',
|
||||
twitter: 'some tw',
|
||||
bio: 'some bio',
|
||||
paid_subscription_started_notification: false,
|
||||
paid_subscription_canceled_notification: true
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports changing password', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Administrators'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await modal.getByRole('button', {name: 'Change password'}).click();
|
||||
|
||||
await modal.getByLabel('New password').fill('newpassword');
|
||||
await modal.getByLabel('Verify password').fill('newpassword');
|
||||
|
||||
await modal.getByRole('button', {name: 'Change password'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Updated'})).toBeVisible();
|
||||
|
||||
expect(lastApiRequests.users.updatePassword.body).toMatchObject({
|
||||
password: [{
|
||||
newPassword: 'newpassword',
|
||||
ne2Password: 'newpassword',
|
||||
oldPassword: '',
|
||||
user_id: responseFixtures.users.users.find(user => user.email === 'administrator@test.com')!.id
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports uploading profile picture', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
|
||||
const wrapper = section.getByTestId('owner-user');
|
||||
await wrapper.hover();
|
||||
await wrapper.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
// Upload profile picture
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
const profileFileChooserPromise = page.waitForEvent('filechooser');
|
||||
|
||||
await modal.locator('label[for=avatar]').click();
|
||||
|
||||
const profileFileChooser = await profileFileChooserPromise;
|
||||
await profileFileChooser.setFiles(`${__dirname}/../../../utils/images/image.png`);
|
||||
|
||||
await expect(modal.locator('#avatar')).toHaveAttribute('src', 'http://example.com/image.png');
|
||||
|
||||
// Upload cover image
|
||||
|
||||
const coverFileChooserPromise = page.waitForEvent('filechooser');
|
||||
|
||||
await modal.locator('label[for=cover-image]').click();
|
||||
|
||||
const coverFileChooser = await coverFileChooserPromise;
|
||||
await coverFileChooser.setFiles(`${__dirname}/../../../utils/images/image.png`);
|
||||
|
||||
await expect(modal.locator('#cover-image')).toHaveAttribute('src', 'http://example.com/image.png');
|
||||
|
||||
// Save the user
|
||||
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
||||
|
||||
expect(lastApiRequests.users.edit.body).toMatchObject({
|
||||
users: [{
|
||||
email: 'owner@test.com',
|
||||
profile_image: 'http://example.com/image.png',
|
||||
cover_image: 'http://example.com/image.png'
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
90
apps/admin-x-settings/test/e2e/general/users/roles.test.ts
Normal file
90
apps/admin-x-settings/test/e2e/general/users/roles.test.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
|
||||
test.describe('User roles', async () => {
|
||||
test('Shows users under their role', async ({page}) => {
|
||||
await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
|
||||
await expect(section.getByTestId('owner-user')).toHaveText(/owner@test\.com/);
|
||||
|
||||
await expect(section.getByRole('tab')).toHaveText([
|
||||
'Administrators',
|
||||
'Editors',
|
||||
'Authors',
|
||||
'Contributors',
|
||||
'Invited'
|
||||
]);
|
||||
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Administrators'}).click();
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveText(/administrator@test\.com/);
|
||||
|
||||
await section.getByRole('tab', {name: 'Editors'}).click();
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveText(/editor@test\.com/);
|
||||
|
||||
await section.getByRole('tab', {name: 'Authors'}).click();
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveText(/author@test\.com/);
|
||||
|
||||
await section.getByRole('tab', {name: 'Contributors'}).click();
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveText(/contributor@test\.com/);
|
||||
});
|
||||
|
||||
test('Supports changing user role', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
users: {
|
||||
edit: {
|
||||
users: [{
|
||||
...responseFixtures.users.users.find(user => user.email === 'author@test.com')!,
|
||||
roles: [responseFixtures.roles.roles.find(role => role.name === 'Editor')!]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('users');
|
||||
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
||||
|
||||
await section.getByRole('tab', {name: 'Authors'}).click();
|
||||
|
||||
const listItem = activeTab.getByTestId('user-list-item').last();
|
||||
await listItem.hover();
|
||||
await listItem.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
const modal = page.getByTestId('user-detail-modal');
|
||||
|
||||
await modal.locator('input[value=editor]').check();
|
||||
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
|
||||
await expect(activeTab).toHaveText(/No users found/);
|
||||
|
||||
await section.getByRole('tab', {name: 'Editors'}).click();
|
||||
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveCount(2);
|
||||
|
||||
await expect(activeTab.getByTestId('user-list-item')).toHaveText([
|
||||
/author@test\.com/,
|
||||
/editor@test\.com/
|
||||
]);
|
||||
|
||||
expect(lastApiRequests.users.edit.body).toMatchObject({
|
||||
users: [{
|
||||
email: 'author@test.com',
|
||||
roles: [{
|
||||
name: 'Editor'
|
||||
}]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,26 +1,47 @@
|
|||
import {CustomThemeSettingsResponseType, ImagesResponseType, InvitesResponseType, RolesResponseType, SettingsResponseType, SiteResponseType, UsersResponseType} from '../../src/utils/api';
|
||||
import {Page, Request} from '@playwright/test';
|
||||
import {readFileSync} from 'fs';
|
||||
|
||||
const responseFixtures = {
|
||||
settings: JSON.parse(readFileSync(`${__dirname}/responses/settings.json`).toString()),
|
||||
site: JSON.parse(readFileSync(`${__dirname}/responses/site.json`).toString()),
|
||||
custom_theme_settings: JSON.parse(readFileSync(`${__dirname}/responses/custom_theme_settings.json`).toString())
|
||||
export const responseFixtures = {
|
||||
settings: JSON.parse(readFileSync(`${__dirname}/responses/settings.json`).toString()) as SettingsResponseType,
|
||||
users: JSON.parse(readFileSync(`${__dirname}/responses/users.json`).toString()) as UsersResponseType,
|
||||
me: JSON.parse(readFileSync(`${__dirname}/responses/me.json`).toString()) as UsersResponseType,
|
||||
roles: JSON.parse(readFileSync(`${__dirname}/responses/roles.json`).toString()) as RolesResponseType,
|
||||
site: JSON.parse(readFileSync(`${__dirname}/responses/site.json`).toString()) as SiteResponseType,
|
||||
invites: JSON.parse(readFileSync(`${__dirname}/responses/invites.json`).toString()) as InvitesResponseType,
|
||||
custom_theme_settings: JSON.parse(readFileSync(`${__dirname}/responses/custom_theme_settings.json`).toString()) as CustomThemeSettingsResponseType
|
||||
};
|
||||
|
||||
interface Responses {
|
||||
settings?: {
|
||||
browse?: any
|
||||
edit?: any
|
||||
browse?: SettingsResponseType
|
||||
edit?: SettingsResponseType
|
||||
}
|
||||
users?: {
|
||||
browse?: UsersResponseType
|
||||
currentUser?: UsersResponseType
|
||||
edit?: UsersResponseType
|
||||
delete?: UsersResponseType
|
||||
updatePassword?: UsersResponseType
|
||||
makeOwner?: UsersResponseType
|
||||
}
|
||||
roles?: {
|
||||
browse?: RolesResponseType
|
||||
}
|
||||
invites?: {
|
||||
browse?: InvitesResponseType
|
||||
add?: InvitesResponseType
|
||||
delete?: InvitesResponseType
|
||||
}
|
||||
site?: {
|
||||
browse?: any
|
||||
browse?: SiteResponseType
|
||||
}
|
||||
images?: {
|
||||
upload?: any
|
||||
upload?: ImagesResponseType
|
||||
}
|
||||
custom_theme_settings?: {
|
||||
browse?: any
|
||||
edit?: any
|
||||
browse?: CustomThemeSettingsResponseType
|
||||
edit?: CustomThemeSettingsResponseType
|
||||
}
|
||||
previewHtml?: {
|
||||
homepage?: string
|
||||
|
@ -38,6 +59,22 @@ type LastRequests = {
|
|||
browse: RequestRecord
|
||||
edit: RequestRecord
|
||||
}
|
||||
users: {
|
||||
browse: RequestRecord
|
||||
currentUser: RequestRecord
|
||||
edit: RequestRecord
|
||||
delete: RequestRecord
|
||||
updatePassword: RequestRecord
|
||||
makeOwner: RequestRecord
|
||||
}
|
||||
roles: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
invites: {
|
||||
browse: RequestRecord
|
||||
add: RequestRecord
|
||||
delete: RequestRecord
|
||||
}
|
||||
site: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
|
@ -56,6 +93,9 @@ type LastRequests = {
|
|||
export async function mockApi({page,responses}: {page: Page, responses?: Responses}) {
|
||||
const lastApiRequests: LastRequests = {
|
||||
settings: {browse: {}, edit: {}},
|
||||
users: {browse: {}, currentUser: {}, edit: {}, delete: {}, updatePassword: {}, makeOwner: {}},
|
||||
roles: {browse: {}},
|
||||
invites: {browse: {}, add: {}, delete: {}},
|
||||
site: {browse: {}},
|
||||
images: {upload: {}},
|
||||
custom_theme_settings: {browse: {}, edit: {}},
|
||||
|
@ -77,6 +117,76 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/\?/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.users?.browse ?? responseFixtures.users,
|
||||
updateLastRequest: lastApiRequests.users.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/me\//,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.users?.currentUser ?? responseFixtures.me,
|
||||
updateLastRequest: lastApiRequests.users.currentUser
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/(\d+|\w{24})\//,
|
||||
respondTo: {
|
||||
PUT: {
|
||||
body: responses?.users?.edit ?? responseFixtures.users,
|
||||
updateLastRequest: lastApiRequests.users.edit
|
||||
},
|
||||
DELETE: {
|
||||
body: responses?.users?.delete ?? responseFixtures.users,
|
||||
updateLastRequest: lastApiRequests.users.delete
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/owner\//,
|
||||
respondTo: {
|
||||
PUT: {
|
||||
body: responses?.users?.makeOwner ?? responseFixtures.users,
|
||||
updateLastRequest: lastApiRequests.users.makeOwner
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/password\//,
|
||||
respondTo: {
|
||||
PUT: {
|
||||
body: responses?.users?.updatePassword ?? responseFixtures.users,
|
||||
updateLastRequest: lastApiRequests.users.updatePassword
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/roles\/\?/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.roles?.browse ?? responseFixtures.roles,
|
||||
updateLastRequest: lastApiRequests.roles.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/site\//,
|
||||
|
@ -99,6 +209,32 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/invites\//,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.invites?.browse ?? responseFixtures.invites,
|
||||
updateLastRequest: lastApiRequests.invites.browse
|
||||
},
|
||||
POST: {
|
||||
body: responses?.invites?.add ?? responseFixtures.invites,
|
||||
updateLastRequest: lastApiRequests.invites.add
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/invites\/\w{24}\//,
|
||||
respondTo: {
|
||||
DELETE: {
|
||||
body: responses?.invites?.delete ?? responseFixtures.invites,
|
||||
updateLastRequest: lastApiRequests.invites.delete
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/custom_theme_settings\/$/,
|
||||
|
|
23
apps/admin-x-settings/test/utils/responses/invites.json
Normal file
23
apps/admin-x-settings/test/utils/responses/invites.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"invites": [
|
||||
{
|
||||
"id": "6498dc1d33daa20df683e909",
|
||||
"role_id": "645453f3d254799990dd0e18",
|
||||
"status": "sent",
|
||||
"email": "invitee@test.com",
|
||||
"expires": 1687655172000,
|
||||
"created_at": "2023-06-23T00:30:21.000Z",
|
||||
"updated_at": "2023-06-23T00:30:21.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 15,
|
||||
"pages": 1,
|
||||
"total": 1,
|
||||
"next": null,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}
|
32
apps/admin-x-settings/test/utils/responses/me.json
Normal file
32
apps/admin-x-settings/test/utils/responses/me.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"users": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Owner User",
|
||||
"slug": "owner",
|
||||
"email": "owner@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-25T23:34:33.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-06-25T23:34:33.000Z",
|
||||
"url": "http://localhost:2368/author/owner/"
|
||||
}
|
||||
]
|
||||
}
|
74
apps/admin-x-settings/test/utils/responses/roles.json
Normal file
74
apps/admin-x-settings/test/utils/responses/roles.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e16",
|
||||
"name": "Administrator",
|
||||
"description": "Administrators",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e17",
|
||||
"name": "Editor",
|
||||
"description": "Editors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e18",
|
||||
"name": "Author",
|
||||
"description": "Authors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e19",
|
||||
"name": "Contributor",
|
||||
"description": "Contributors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1a",
|
||||
"name": "Owner",
|
||||
"description": "Blog Owner",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1b",
|
||||
"name": "Admin Integration",
|
||||
"description": "External Apps",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1c",
|
||||
"name": "Ghost Explore Integration",
|
||||
"description": "Internal Integration for the Ghost Explore directory",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1d",
|
||||
"name": "Self-Serve Migration Integration",
|
||||
"description": "Internal Integration for the Self-Serve migration tool",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1e",
|
||||
"name": "DB Backup Integration",
|
||||
"description": "Internal DB Backup Client",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1f",
|
||||
"name": "Scheduler Integration",
|
||||
"description": "Internal Scheduler Client",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
]
|
||||
}
|
199
apps/admin-x-settings/test/utils/responses/users.json
Normal file
199
apps/admin-x-settings/test/utils/responses/users.json
Normal file
|
@ -0,0 +1,199 @@
|
|||
{
|
||||
"users": [
|
||||
{
|
||||
"id": "6498da2533daa20df683e906",
|
||||
"name": "Contributor User",
|
||||
"slug": "contributor",
|
||||
"email": "contributor@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-26T00:21:58.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-06-26T00:21:57.000Z",
|
||||
"updated_at": "2023-06-26T00:21:58.000Z",
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e19",
|
||||
"name": "Contributor",
|
||||
"description": "Contributors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:2368/404/"
|
||||
},
|
||||
{
|
||||
"id": "6498da0233daa20df683e903",
|
||||
"name": "Author User",
|
||||
"slug": "author",
|
||||
"email": "author@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-26T00:21:22.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-06-26T00:21:22.000Z",
|
||||
"updated_at": "2023-06-26T00:21:22.000Z",
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e18",
|
||||
"name": "Author",
|
||||
"description": "Authors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:2368/404/"
|
||||
},
|
||||
{
|
||||
"id": "6498d9e833daa20df683e900",
|
||||
"name": "Administrator User",
|
||||
"slug": "administrator",
|
||||
"email": "administrator@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-26T00:20:57.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-06-26T00:20:56.000Z",
|
||||
"updated_at": "2023-06-26T00:20:57.000Z",
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e16",
|
||||
"name": "Administrator",
|
||||
"description": "Administrators",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:2368/404/"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Owner User",
|
||||
"slug": "owner",
|
||||
"email": "owner@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-25T23:34:33.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-06-25T23:34:33.000Z",
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e1a",
|
||||
"name": "Owner",
|
||||
"description": "Blog Owner",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:2368/author/owner/"
|
||||
},
|
||||
{
|
||||
"id": "6493dead1e89fd3bdee8529c",
|
||||
"name": "Editor User",
|
||||
"slug": "editor",
|
||||
"email": "editor@test.com",
|
||||
"profile_image": null,
|
||||
"cover_image": null,
|
||||
"bio": null,
|
||||
"website": null,
|
||||
"location": null,
|
||||
"facebook": null,
|
||||
"twitter": null,
|
||||
"accessibility": null,
|
||||
"status": "active",
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"tour": null,
|
||||
"last_seen": "2023-06-22T05:39:58.000Z",
|
||||
"comment_notifications": true,
|
||||
"free_member_signup_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"mention_notifications": true,
|
||||
"milestone_notifications": true,
|
||||
"created_at": "2023-06-22T05:39:57.000Z",
|
||||
"updated_at": "2023-06-22T05:39:58.000Z",
|
||||
"roles": [
|
||||
{
|
||||
"id": "645453f3d254799990dd0e17",
|
||||
"name": "Editor",
|
||||
"description": "Editors",
|
||||
"created_at": "2023-05-05T00:55:15.000Z",
|
||||
"updated_at": "2023-05-05T00:55:15.000Z"
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:2368/404/"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": "all",
|
||||
"pages": 1,
|
||||
"total": 5,
|
||||
"next": null,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue