98 lines
5.8 KiB
TypeScript
98 lines
5.8 KiB
TypeScript
// 超级管理员 UI 门控(计划 2026-10-19-0113 R3/R4/R8):身份标签可见;建号为管理员与
|
|
// 任命/撤销入口只对超级管理员出现;超级管理员不可降级、停用或被普通管理员维护。
|
|
// 服务端授权仍为权威,这里只锁定前端呈现与请求体。
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { createElement } from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import { api } from '../src/api'
|
|
import { auth } from '../src/auth'
|
|
import { accountActionPolicy, accountIdentityKind, canAppointAdmins } from '../src/accountAuthority'
|
|
import { AdminAppointmentButton, CreateAdminOption, IdentityChip } from '../src/components/AccountUsersPanel'
|
|
|
|
afterEach(() => vi.restoreAllMocks())
|
|
|
|
const superAdmin = { isAdmin: true, isSuperAdmin: true }
|
|
const admin = { isAdmin: true, isSuperAdmin: false }
|
|
const user = { isAdmin: false, isSuperAdmin: false }
|
|
const noop = () => undefined
|
|
|
|
describe('身份标签', () => {
|
|
it('超级管理员、管理员与普通用户各有可辨识标签', () => {
|
|
expect(accountIdentityKind(superAdmin)).toBe('super')
|
|
expect(accountIdentityKind(admin)).toBe('admin')
|
|
expect(accountIdentityKind(user)).toBe('user')
|
|
expect(renderToStaticMarkup(createElement(IdentityChip, { account: superAdmin }))).toContain('超级管理员')
|
|
expect(renderToStaticMarkup(createElement(IdentityChip, { account: admin }))).toContain('>管理员<')
|
|
expect(renderToStaticMarkup(createElement(IdentityChip, { account: user }))).toContain('普通用户')
|
|
})
|
|
it('isAdmin 不推导超级管理员身份', () => {
|
|
expect(accountIdentityKind({ isAdmin: true })).toBe('admin')
|
|
})
|
|
})
|
|
|
|
describe('管理员任命入口只对超级管理员开放', () => {
|
|
it('建号:超级管理员看到「创建为管理员账号」,普通管理员只见普通建号说明', () => {
|
|
expect(canAppointAdmins(superAdmin)).toBe(true)
|
|
expect(canAppointAdmins(admin)).toBe(false)
|
|
expect(canAppointAdmins(null)).toBe(false)
|
|
const superHtml = renderToStaticMarkup(createElement(CreateAdminOption, { me: superAdmin, checked: false, disabled: false, onChange: noop }))
|
|
expect(superHtml).toContain('type="checkbox"')
|
|
expect(superHtml).toContain('创建为管理员账号')
|
|
const adminHtml = renderToStaticMarkup(createElement(CreateAdminOption, { me: admin, checked: false, disabled: false, onChange: noop }))
|
|
expect(adminHtml).not.toContain('checkbox')
|
|
expect(adminHtml).toContain('仅超级管理员可创建或任命管理员')
|
|
})
|
|
it('任命/撤销:超级管理员可任命普通用户、撤销管理员;普通管理员看不到任何任命按钮', () => {
|
|
expect(accountActionPolicy(superAdmin, user).adminChange).toBe('promote')
|
|
expect(accountActionPolicy(superAdmin, admin).adminChange).toBe('demote')
|
|
expect(renderToStaticMarkup(createElement(AdminAppointmentButton, { me: superAdmin, account: user, busy: false, onChange: noop }))).toContain('设为管理员')
|
|
expect(renderToStaticMarkup(createElement(AdminAppointmentButton, { me: superAdmin, account: admin, busy: false, onChange: noop }))).toContain('撤销管理员')
|
|
for (const target of [user, admin, superAdmin]) {
|
|
expect(accountActionPolicy(admin, target).adminChange).toBeNull()
|
|
expect(renderToStaticMarkup(createElement(AdminAppointmentButton, { me: admin, account: target, busy: false, onChange: noop }))).toBe('')
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('超级管理员账号受保护', () => {
|
|
it('超级管理员本人不可被降级或停用,也不走管理员重置密码', () => {
|
|
const self = accountActionPolicy(superAdmin, superAdmin)
|
|
expect(self).toMatchObject({ adminChange: null, toggleEnabled: false, resetPassword: false, maintain: true })
|
|
expect(self.protectedReason).toContain('不能停用、降级')
|
|
expect(renderToStaticMarkup(createElement(AdminAppointmentButton, { me: superAdmin, account: superAdmin, busy: false, onChange: noop }))).toBe('')
|
|
})
|
|
it('普通管理员不能编辑、停用、降级或重置超级管理员,并获得明确说明', () => {
|
|
const policy = accountActionPolicy(admin, superAdmin)
|
|
expect(policy).toMatchObject({ maintain: false, toggleEnabled: false, resetPassword: false, adminChange: null })
|
|
expect(policy.protectedReason).toContain('超级管理员账号受保护')
|
|
})
|
|
it('普通账号的既有维护能力不受影响', () => {
|
|
expect(accountActionPolicy(admin, user)).toMatchObject({ maintain: true, toggleEnabled: true, resetPassword: true, protectedReason: '' })
|
|
})
|
|
})
|
|
|
|
describe('账号管理请求体', () => {
|
|
const stub = (body: unknown) => vi.spyOn(auth, 'fetch').mockImplementation(async () => new Response(JSON.stringify(body)))
|
|
|
|
it('adminSetUserAdmin 以乐观版本 PUT /api/admin/users/:userId/admin', async () => {
|
|
const request = stub({ userId: 'u 1' })
|
|
await api.adminSetUserAdmin('u 1', 7, true)
|
|
const [url, options] = request.mock.calls[0]
|
|
expect(url).toBe('/api/admin/users/u%201/admin')
|
|
expect(options?.method).toBe('PUT')
|
|
expect(JSON.parse(String(options?.body))).toEqual({ expectedStateVersion: 7, isAdmin: true })
|
|
})
|
|
it('建号只在请求管理员身份时携带 isAdmin', async () => {
|
|
const request = stub({ userId: 'u1', owner: 'alice' })
|
|
await api.createManagedUser('alice', { name: 'A' })
|
|
await api.createManagedUser('bob', {}, true)
|
|
await api.createAdminUser('carol', 'C')
|
|
await api.createAdminUser('dave', 'D', undefined, true)
|
|
const bodies = request.mock.calls.map(call => JSON.parse(String(call[1]?.body)))
|
|
expect(bodies[0]).toEqual({ owner: 'alice', profile: { name: 'A' } })
|
|
expect(bodies[1]).toEqual({ owner: 'bob', profile: {}, isAdmin: true })
|
|
expect(bodies[2]).toEqual({ owner: 'carol', profile: { name: 'C' } })
|
|
expect(bodies[3]).toEqual({ owner: 'dave', profile: { name: 'D' }, isAdmin: true })
|
|
})
|
|
})
|