Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/dashboard/e2e/tests/settings/profile.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';

import { VendureAdminClient } from '../../utils/vendure-admin-client.js';

test.describe('Profile', () => {
test('should display profile page with form fields', async ({ page }) => {
await page.goto('/profile');
Expand Down Expand Up @@ -87,4 +89,65 @@ test.describe('Profile', () => {
// Reset to original value (don't submit)
await firstNameInput.fill(originalValue);
});

// #5037 — an admin without UpdateAdministrator permission must still be able to
// edit their own profile (the page uses updateActiveAdministrator, gated by Owner)
test('should allow an admin without UpdateAdministrator permission to update own profile', async ({
page,
browser,
}) => {
const suffix = Date.now();
const emailAddress = `restricted-${suffix}@test.com`;
const password = 'test-password';

const client = new VendureAdminClient(page);
await client.login();
const { createRole } = await client.gql(
`mutation ($input: CreateRoleInput!) { createRole(input: $input) { id } }`,
{
input: {
code: `restricted-${suffix}`,
description: 'No admin permissions',
permissions: ['ReadCatalog'],
},
},
);
await client.gql(
`mutation ($input: CreateAdministratorInput!) { createAdministrator(input: $input) { id } }`,
{
input: {
firstName: 'Restricted',
lastName: 'Admin',
emailAddress,
password,
roleIds: [createRole.id],
},
},
);

// Fresh context so we browse as the restricted admin, not the superadmin
const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const restrictedPage = await context.newPage();
await restrictedPage.goto('/login');
await restrictedPage.getByPlaceholder('Email').fill(emailAddress);
await restrictedPage.getByPlaceholder('Password').fill(password);
await restrictedPage.getByRole('button', { name: 'Sign in' }).click();
await expect(restrictedPage).not.toHaveURL(/\/login/, { timeout: 15_000 });

await restrictedPage.goto('/profile');
const firstNameField = restrictedPage.locator('[data-slot="field"]').filter({
has: restrictedPage.locator('[data-slot="field-label"]').getByText('First name', { exact: true }),
});
await firstNameField.getByRole('textbox').fill('Renamed');
await restrictedPage.getByRole('button', { name: 'Update' }).click();

await expect(
restrictedPage.locator('[data-sonner-toast]').filter({ hasText: 'Successfully updated profile' }),
).toBeVisible({ timeout: 10_000 });

await restrictedPage.reload();
await expect(firstNameField.getByRole('textbox')).toHaveValue('Renamed');

await context.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export const activeAdministratorDocument = graphql(`
}
`);

export const updateAdministratorDocument = graphql(`
mutation UpdateAdministrator($input: UpdateAdministratorInput!) {
updateAdministrator(input: $input) {
export const updateActiveAdministratorDocument = graphql(`
mutation UpdateActiveAdministrator($input: UpdateActiveAdministratorInput!) {
updateActiveAdministrator(input: $input) {
id
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
import { Trans, useLingui } from '@lingui/react/macro';
import { createFileRoute } from '@tanstack/react-router';
import { toast } from 'sonner';
import { activeAdministratorDocument, updateAdministratorDocument } from './profile.graphql.js';
import { activeAdministratorDocument, updateActiveAdministratorDocument } from './profile.graphql.js';

const pageId = 'profile';

Expand Down Expand Up @@ -49,11 +49,10 @@ function ProfilePage() {
const { form, submitHandler, isPending, entity } = useDetailPage({
queryDocument: activeAdministratorDocument,
entityField: 'activeAdministrator',
updateDocument: updateAdministratorDocument,
updateDocument: updateActiveAdministratorDocument,
pageId,
setValuesForUpdate: entity => {
return {
id: entity.id,
firstName: entity.firstName,
lastName: entity.lastName,
emailAddress: entity.emailAddress,
Expand Down
Loading