agent-exec transport 91fa500d0ed8f7daca02bd46ba843a9a/b201c6aced99
governance / governance (pull_request) Successful in 13s
ci / test-and-build (pull_request) Successful in 46s

This commit is contained in:
agent-exec-transport
2026-10-20 10:48:43 +00:00
parent c6df2dc180
commit 1fe321240a
5 changed files with 83 additions and 8 deletions
+61
View File
@@ -0,0 +1,61 @@
// 管理端用户面板两处缺陷的回归锁定:
// (1) getUserRole 的 role 摘要不带 permissions(线上实测),projectCreateSources 不得因此抛异常;
// 展示来源时用已加载角色列表里同一 roleId 的条目补齐权限,不新增请求。
// (2) 一次性登录密码按服务端严格解码要求发送 JSON 对象体。
import { afterEach, describe, expect, it, vi } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { api, type RoleRecord } from '../src/api'
import { auth } from '../src/auth'
import { ProjectCreateSourceNote, roleWithPermissions } from '../src/components/AccountUsersPanel'
import { projectCreateSources } from '../src/rolePermissions'
const listed: RoleRecord = {
roleId: 'role-pm', name: '项目经理', description: '研发', enabled: true, roleVersion: 3, bindingCount: 1,
permissions: [
{ actionId: 'project.create', allowed: true, capAllowed: true, permissionVersion: 2 },
{ actionId: 'document.read', allowed: true, capAllowed: true, permissionVersion: 1 },
],
}
/** getUserRole 的真实响应形状:有角色字段、没有 permissions。 */
const summary = { roleId: 'role-pm', name: '项目经理', description: '研发', enabled: true, roleVersion: 3, bindingCount: 1 }
afterEach(() => vi.restoreAllMocks())
describe('角色摘要缺少 permissions 时的创建能力来源', () => {
it('把缺失的 permissions 当作空集,不抛异常也不虚构角色来源', () => {
expect(projectCreateSources({ name: '项目经理' }, [])).toEqual([])
expect(projectCreateSources({ name: '项目经理' }, ['project.create'])).toEqual(['明确系统授权'])
expect(projectCreateSources({ ...summary, permissions: undefined }, [])).toEqual([])
expect(renderToStaticMarkup(<ProjectCreateSourceNote role={{ name: '项目经理' }} grants={[]} />))
.toContain('被加入项目后由成员关系提供基础协作')
})
it('用角色列表里同一 roleId 的权限补齐来源', () => {
const resolved = roleWithPermissions(summary, [listed])
expect(resolved?.permissions?.some(item => item.actionId === 'project.create')).toBe(true)
expect(renderToStaticMarkup(<ProjectCreateSourceNote role={resolved} grants={[]} />))
.toContain('创建项目来源:职位角色「项目经理」')
})
it('列表里没有对应条目时按无角色默认权限处理,角色自带权限时不被列表覆盖', () => {
expect(projectCreateSources(roleWithPermissions(summary, []), [])).toEqual([])
const denied = { ...listed, roleId: 'role-other', permissions: [{ actionId: 'project.create', allowed: false, capAllowed: false, permissionVersion: 1 }] }
expect(projectCreateSources(roleWithPermissions({ ...summary, permissions: denied.permissions }, [denied]), [])).toEqual([])
})
})
describe('管理员一次性登录密码请求体', () => {
it('发送 POST + JSON 对象体,避免服务端 400 EOF', async () => {
const issued = { owner: 'alice', id: 'otp_123', oneTimePassword: 'otp-0001', mustChangePassword: true }
const request = vi.spyOn(auth, 'fetch').mockResolvedValue(new Response(JSON.stringify(issued)))
const out = await api.adminIssueOneTimePassword('alice')
const [url, options] = request.mock.calls[0]
expect(url).toBe('/api/admin/users/alice/one-time-password')
expect(options?.method).toBe('POST')
expect(options?.headers).toEqual({ 'Content-Type': 'application/json' })
expect(JSON.parse(String(options?.body))).toEqual({})
expect(out).toEqual(issued)
})
})
+4 -1
View File
@@ -21,7 +21,10 @@ describe('管理员一次性密码发放', () => {
const out = await api.adminIssueOneTimePassword('alice')
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith('/api/admin/users/alice/one-time-password', { method: 'POST' })
// 服务端严格解码 JSON 对象体:无体无 Content-Type 会被拒为 400 EOF。
expect(fetchSpy).toHaveBeenCalledWith('/api/admin/users/alice/one-time-password', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
})
expect(out).toEqual(response)
})