From be51f173f5ba4d41bcc95c8a51702a802c87ee32 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 22 Jun 2026 10:15:49 +0800 Subject: [PATCH] fix(web): ISSUE-56 show security settings for local accounts Signed-off-by: dongmucat <1127093059@qq.com> --- web/e2e/user-menu-security-settings.spec.ts | 100 ++++++++++++++++++++ web/src/shared/components/user-menu.test.ts | 22 ++--- web/src/shared/components/user-menu.tsx | 6 +- 3 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 web/e2e/user-menu-security-settings.spec.ts diff --git a/web/e2e/user-menu-security-settings.spec.ts b/web/e2e/user-menu-security-settings.spec.ts new file mode 100644 index 00000000..81732dd9 --- /dev/null +++ b/web/e2e/user-menu-security-settings.spec.ts @@ -0,0 +1,100 @@ +import { expect, test, type Page } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' + +type AuthUser = { + userId: string + displayName: string + email: string + avatarUrl: string + oauthProvider?: string + platformRoles: string[] +} + +function apiEnvelope(data: unknown) { + return { + code: 0, + msg: 'ok', + data, + timestamp: new Date().toISOString(), + requestId: 'user-menu-security-settings', + } +} + +async function mockSession(page: Page, user: AuthUser) { + await page.route('**/api/v1/auth/me', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(apiEnvelope(user)), + }) + }) + + await page.route('**/api/web/me/namespaces', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(apiEnvelope([])), + }) + }) + + await page.route('**/api/web/notifications/unread-count', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(apiEnvelope({ count: 0 })), + }) + }) + + await page.route('**/api/web/notifications/sse', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/event-stream', + body: '', + }) + }) +} + +async function openUserMenu(page: Page, displayName: string) { + await page.goto('/settings/security') + await expect(page.getByRole('heading', { name: 'Security Settings' })).toBeVisible() + await page.getByRole('button', { name: displayName }).click() + await expect(page.getByRole('menu')).toBeVisible() +} + +test.describe('User menu security settings visibility', () => { + test.use({ baseURL: 'http://127.0.0.1:3000' }) + + test.beforeEach(async ({ page }) => { + await setEnglishLocale(page) + }) + + test('shows security settings for local password accounts', async ({ page }) => { + await mockSession(page, { + userId: 'docker-admin', + displayName: 'Platform Admin', + email: 'admin@skillhub.local', + avatarUrl: '', + oauthProvider: 'local', + platformRoles: ['SUPER_ADMIN'], + }) + + await openUserMenu(page, 'Platform Admin') + + await expect(page.getByRole('menu').getByRole('link', { name: 'Security Settings' })).toBeVisible() + }) + + test('hides security settings for OAuth provider accounts', async ({ page }) => { + await mockSession(page, { + userId: 'oauth-admin', + displayName: 'OAuth Admin', + email: 'oauth-admin@example.com', + avatarUrl: '', + oauthProvider: 'github', + platformRoles: ['SUPER_ADMIN'], + }) + + await openUserMenu(page, 'OAuth Admin') + + await expect(page.getByRole('menu').getByRole('link', { name: 'Security Settings' })).toHaveCount(0) + }) +}) diff --git a/web/src/shared/components/user-menu.test.ts b/web/src/shared/components/user-menu.test.ts index a1877caf..333f7e08 100644 --- a/web/src/shared/components/user-menu.test.ts +++ b/web/src/shared/components/user-menu.test.ts @@ -1,18 +1,12 @@ import { describe, expect, it } from 'vitest' -import * as mod from './user-menu' +import { canShowSecuritySettings } from './user-menu' -/** - * UserMenu is a React component that renders a hover/click dropdown menu with - * role-based navigation links (dashboard, reviews, admin, etc.) and logout. - * Internal helpers (hasRole, closeMenu, handleMouseEnter/Leave) and the - * menuItemClassName constant are scoped inside the component function. - * There are no exported pure helpers or constants to test here. - * - * We verify the module shape so downstream consumers break fast - * if the export contract changes. - */ -describe('user-menu module exports', () => { - it('exports the UserMenu component', () => { - expect(mod.UserMenu).toBeTypeOf('function') +describe('canShowSecuritySettings', () => { + it('allows local password accounts to open security settings', () => { + expect(canShowSecuritySettings({ oauthProvider: 'local' })).toBe(true) + }) + + it('keeps security settings hidden for OAuth provider accounts', () => { + expect(canShowSecuritySettings({ oauthProvider: 'github' })).toBe(false) }) }) diff --git a/web/src/shared/components/user-menu.tsx b/web/src/shared/components/user-menu.tsx index 1cb10a31..6a604e1b 100644 --- a/web/src/shared/components/user-menu.tsx +++ b/web/src/shared/components/user-menu.tsx @@ -21,6 +21,10 @@ interface UserMenuProps { triggerClassName?: string } +export function canShowSecuritySettings(user: Pick) { + return !user.oauthProvider || user.oauthProvider === 'local' +} + export function UserMenu({ user, triggerClassName }: UserMenuProps) { const { t } = useTranslation() const queryClient = useQueryClient() @@ -37,7 +41,7 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) { const isAuditor = hasRole('AUDITOR') || hasRole('SUPER_ADMIN') const isSuperAdmin = hasRole('SUPER_ADMIN') const reviewCenterVisible = canAccessReviewCenter(user.platformRoles, myNamespaces) - const isLocalAccount = !user.oauthProvider + const isLocalAccount = canShowSecuritySettings(user) const open = isHovered || isClickOpen const clearCloseTimer = () => {