fix(web): ISSUE-56 show security settings for local accounts

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-06-22 10:15:49 +08:00
parent dc185861d4
commit be51f173f5
3 changed files with 113 additions and 15 deletions

View file

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

View file

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

View file

@ -21,6 +21,10 @@ interface UserMenuProps {
triggerClassName?: string
}
export function canShowSecuritySettings(user: Pick<User, 'oauthProvider'>) {
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 = () => {