0
Fork 0
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:
Jono M 2023-06-26 15:12:46 +12:00 committed by GitHub
parent 0281a30fb4
commit 9e325d6b38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1042 additions and 29 deletions

View file

@ -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;

View file

@ -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
}
);
};
};

View file

@ -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;

View file

@ -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);

View file

@ -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}
>

View file

@ -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)) {

View file

@ -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} &mdash; <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} &mdash; <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} />

View file

@ -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;
}[];
}

View 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
}]
});
});
});

View 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}`));
});
});

View 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'
}]
});
});
});

View 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'
}]
}]
});
});
});

View file

@ -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\/$/,

View 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
}
}
}

View 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/"
}
]
}

View 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"
}
]
}

View 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
}
}
}