Files

217 lines
15 KiB
JavaScript

function id(value) { return value == null ? '' : String(value) }
export async function verifyRevokedSessions(context, reason, sessions, accessPath = '/api/platform/v1/orders/status') {
const options = { scenario: 'unified-session-revocation', deploymentBlocker: true }
for (const [index, session] of sessions.entries()) {
const complete = Boolean(session?.accessToken && session?.refreshToken)
context.assert('system', `${reason}第${index + 1}组会话凭据完整`, null, complete,
`session=${index + 1}`, options)
if (!complete) throw new Error('会话撤销检查缺少完整 Access/Refresh 对')
const access = await context.request('GET', accessPath, undefined, session.accessToken)
context.assert('system', `${reason}旧Access失效`, access,
access.http === 401 && String(access.body?.code) === 'A0401',
`session=${index + 1}, HTTP=${access.http}, code=${access.body?.code || 'missing'}`, options)
const refresh = await context.request('POST', '/api/auth/refresh', { refreshToken: session.refreshToken }, '')
context.assert('system', `${reason}旧Refresh不能恢复会话`, refresh,
[200, 401].includes(refresh.http) && String(refresh.body?.code) === 'A0401',
`session=${index + 1}, HTTP=${refresh.http}, code=${refresh.body?.code || 'missing'}`, options)
}
}
export async function run(context) {
const module = 'system'
const suffix = context.marker.replace(/\D/g, '').slice(-12)
const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/permissions')
const permissions = context.apiSuccess(module, '查询菜单权限资源', permissionsResponse)
context.assert(module, '权限资源返回数组', permissionsResponse, Array.isArray(permissions),
`count=${Array.isArray(permissions) ? permissions.length : 'n/a'}`)
const permissionPayload = {
type: 'BUTTON',
code: `platform:acceptance:${suffix}`,
name: `Admin验收权限-${suffix}`,
parentId: null,
routePath: null,
httpMethod: null,
sortNo: 9999,
}
const createPermissionResponse = await context.request('POST', '/api/platform/v1/iam/permissions', permissionPayload)
const permission = context.apiSuccess(module, '创建自定义权限资源', createPermissionResponse)
const permissionId = id(permission?.id)
context.assert(module, '权限创建字段正确', createPermissionResponse,
permissionId && permission?.permissionCode === permissionPayload.code && permission?.builtIn === false,
`permissionId=${permissionId || 'missing'}`)
if (!permissionId) throw new Error('权限资源创建失败,系统模块无法继续')
context.createdResources.systemPermission = { id: permissionId, code: permissionPayload.code }
const additionalPermissionIds = []
for (const definition of [
{ type: 'MENU', routePath: `/acceptance-${suffix}`, httpMethod: null },
{ type: 'PAGE', routePath: `/acceptance-${suffix}/detail`, httpMethod: null },
{ type: 'API', routePath: `/api/platform/v1/acceptance/${suffix}`, httpMethod: 'GET' },
]) {
const payload = {
type: definition.type,
code: `platform:acceptance:${definition.type.toLowerCase()}:${suffix}`,
name: `Admin验收${definition.type}-${suffix}`,
parentId: null,
routePath: definition.routePath,
httpMethod: definition.httpMethod,
sortNo: 9999,
}
const response = await context.request('POST', '/api/platform/v1/iam/permissions', payload)
const created = context.apiSuccess(module, `创建 ${definition.type} 权限资源`, response)
const createdId = id(created?.id)
context.assert(module, `${definition.type} 类型与端点字段正确回显`, response,
createdId && created?.permissionType === definition.type && created?.routePath === definition.routePath
&& (created?.httpMethod ?? null) === definition.httpMethod,
`permissionId=${createdId || 'missing'}, type=${created?.permissionType || 'missing'}, method=${created?.httpMethod ?? 'null'}`)
if (createdId) additionalPermissionIds.push(createdId)
}
const updatePermissionResponse = await context.request('PUT', `/api/platform/v1/iam/permissions/${permissionId}`, {
name: `${permissionPayload.name}-已编辑`, parentId: null, routePath: null, httpMethod: null, sortNo: 9998,
})
const updatedPermission = context.apiSuccess(module, '编辑自定义权限资源', updatePermissionResponse)
context.assert(module, '权限编辑正确回显', updatePermissionResponse,
updatedPermission?.permissionName === `${permissionPayload.name}-已编辑` && Number(updatedPermission?.sortNo) === 9998,
`permissionId=${permissionId}`)
for (const status of ['DISABLED', 'ACTIVE']) {
const response = await context.request('PUT', `/api/platform/v1/iam/permissions/${permissionId}/status`, { status })
const changed = context.apiSuccess(module, `${status === 'ACTIVE' ? '启用' : '停用'}自定义权限`, response)
context.assert(module, `权限状态回显 ${status}`, response, changed?.status === status, `status=${changed?.status ?? 'missing'}`)
}
const rolePayload = { roleCode: `ADMIN_ACCEPT_${suffix}`, roleName: `Admin验收角色-${suffix}`, description: context.marker }
const createRoleResponse = await context.request('POST', '/api/platform/v1/iam/roles', rolePayload)
const role = context.apiSuccess(module, '创建平台自定义角色', createRoleResponse)
const roleId = id(role?.id)
context.assert(module, '角色创建字段正确', createRoleResponse,
roleId && role?.roleCode === rolePayload.roleCode && role?.roleType === 'CUSTOM', `roleId=${roleId || 'missing'}`)
if (!roleId) throw new Error('角色创建失败,系统模块无法继续')
context.createdResources.systemRole = { id: roleId, code: rolePayload.roleCode, finalStatus: 'DISABLED', retainedBecauseAssigned: true }
const editRoleResponse = await context.request('PUT', `/api/platform/v1/iam/roles/${roleId}`, {
roleName: `${rolePayload.roleName}-已编辑`, description: `${context.marker}-EDITED`,
})
const editedRole = context.apiSuccess(module, '编辑平台自定义角色', editRoleResponse)
context.assert(module, '角色编辑正确回显', editRoleResponse,
editedRole?.roleName === `${rolePayload.roleName}-已编辑`, `roleId=${roleId}`)
const grantResponse = await context.request('PUT', '/api/platform/v1/iam/roles/permissions', { roleId, permissionIds: [permissionId] })
context.apiSuccess(module, '角色全量保存权限', grantResponse)
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
const roles = context.apiSuccess(module, '回查平台角色列表', rolesResponse)
const listedRole = roles?.find?.((item) => id(item.id) === roleId)
context.assert(module, '角色权限 ID 正确回显', rolesResponse,
listedRole && listedRole.permissionIds?.map(id).includes(permissionId),
`permissionIds=${listedRole?.permissionIds?.map(id).join(',') || 'missing'}`)
for (const status of ['DISABLED', 'ACTIVE']) {
const response = await context.request('PUT', `/api/platform/v1/iam/roles/${roleId}/status`, { status })
const changed = context.apiSuccess(module, `${status === 'ACTIVE' ? '启用' : '停用'}自定义角色`, response)
context.assert(module, `角色状态回显 ${status}`, response, changed?.status === status, `status=${changed?.status ?? 'missing'}`)
}
const optionsResponse = await context.request('GET', '/api/platform/v1/iam/users/options')
const options = context.apiSuccess(module, '查询内部用户可选平台角色', optionsResponse)
context.assert(module, '用户选项包含平台与测试角色', optionsResponse,
options?.tenants?.some((item) => id(item.id) === '0') && options?.roles?.some((item) => id(item.id) === roleId),
`tenants=${options?.tenants?.length ?? 'n/a'}, roles=${options?.roles?.length ?? 'n/a'}`)
const phone = await context.findAvailablePhone(module)
const username = `admin_accept_${suffix}`
const initialPassword = `Accept${suffix}A1`
const resetPassword = `Reset${suffix}B2`
const userPayload = {
username, displayName: `Admin验收用户-${suffix}`, phone,
email: `${username}@example.com`, tenantId: '0', roleIds: [roleId],
password: initialPassword, status: 'ACTIVE',
}
const createUserResponse = await context.request('POST', '/api/platform/v1/iam/users', userPayload)
const user = context.apiSuccess(module, '创建平台内部用户', createUserResponse)
const membershipId = id(user?.membershipId)
context.assert(module, '内部用户账号、平台范围和角色正确', createUserResponse,
membershipId && user?.username === username && id(user?.tenantId) === '0' && user?.roleIds?.map(id).includes(roleId),
`membershipId=${membershipId || 'missing'}`)
if (!membershipId) throw new Error('平台内部用户创建失败,系统模块无法继续')
context.createdResources.systemUser = { membershipId, username, finalStatus: 'DISABLED' }
const pageResponse = await context.request('GET', `/api/platform/v1/iam/users?page=1&size=20&keyword=${username}`)
const page = context.apiSuccess(module, '按账号查询平台内部用户', pageResponse)
const listedUser = page?.records?.find((item) => id(item.membershipId) === membershipId)
const missingUserFields = listedUser ? requiredUserFields().filter((field) => !Object.hasOwn(listedUser, field)) : requiredUserFields()
context.assert(module, '用户列表包含新账号且字段完整', pageResponse,
listedUser && missingUserFields.length === 0
&& (!Object.hasOwn(listedUser, 'lastLoginAt') || typeof listedUser.lastLoginAt === 'string'),
`records=${page?.records?.length ?? 'n/a'}, missing=${missingUserFields.join(',') || 'none'}, lastLoginAt=${listedUser?.lastLoginAt ?? 'omitted'}`)
const updateUserPayload = {
displayName: `${userPayload.displayName}-已编辑`, phone,
email: `edited-${userPayload.email}`, tenantId: '0', roleIds: [roleId],
}
const updateUserResponse = await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}`, updateUserPayload)
const updatedUser = context.apiSuccess(module, '编辑平台内部用户', updateUserResponse)
context.assert(module, '用户编辑正确回显', updateUserResponse,
updatedUser?.displayName === updateUserPayload.displayName && updatedUser?.email === updateUserPayload.email,
`membershipId=${membershipId}`)
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: initialPassword }, '')
const loginPassed = loginResponse.http === 200 && String(loginResponse.body?.code) === '200' && Boolean(loginResponse.body?.data?.accessToken)
context.addStep(module, '新建内部用户可使用初始密码登录', loginResponse, loginPassed, `username=${username}`)
const secondLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: initialPassword }, '')
const secondLogin = context.apiSuccess(module, '建立同账号第二个独立会话', secondLoginResponse)
const resetResponse = await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}/password`, { newPassword: resetPassword })
context.apiSuccess(module, '重置平台内部用户密码', resetResponse)
await verifyRevokedSessions(context, '密码重置撤销多端会话', [loginResponse.body?.data, secondLogin])
const resetLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: resetPassword }, '')
const resetLoginPassed = resetLoginResponse.http === 200 && String(resetLoginResponse.body?.code) === '200' && Boolean(resetLoginResponse.body?.data?.accessToken)
context.addStep(module, '内部用户使用重置密码登录', resetLoginResponse, resetLoginPassed, `username=${username}`)
if (context.options.modules.includes('phase4-cross-region-events')) {
const restrictedOverseasToken = resetLoginResponse.body?.data?.accessToken || ''
context.assert(module, '受限角色登录同时签发统一Token', resetLoginResponse,
Boolean(restrictedOverseasToken), `tokenPresent=${Boolean(restrictedOverseasToken)}`,
{ scenario: 'canonical-overseas-permission-denial', deploymentBlocker: true })
if (restrictedOverseasToken) {
const denied = await context.request('GET', '/api/platform/v1/last-leg/orders?page=1&size=1', undefined, restrictedOverseasToken, {})
context.assert(module, '缺少canonical海外订单权限必须403', denied,
(denied.http === 403 || ['403', 'A0403'].includes(String(denied.body?.code)))
&& Boolean(denied.requestId) && Boolean(denied.traceId),
`HTTP=${denied.http}, code=${denied.body?.code ?? 'missing'}, requestId=${denied.requestId || 'missing'}, traceId=${denied.traceId || 'missing'}`,
{ scenario: 'canonical-overseas-permission-denial', deploymentBlocker: true })
}
}
const clearPermissionResponse = await context.request('PUT', '/api/platform/v1/iam/roles/permissions', { roleId, permissionIds: [] })
context.apiSuccess(module, '清空测试角色权限', clearPermissionResponse)
await verifyRevokedSessions(context, '角色权限清空撤销已登录会话', [resetLoginResponse.body?.data])
const activeLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: resetPassword }, '')
const activeLogin = context.apiSuccess(module, '按更新后的权限重新登录', activeLoginResponse)
const disableUserResponse = await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}/status`, { status: 'DISABLED' })
const disabledUser = context.apiSuccess(module, '停用平台内部用户', disableUserResponse)
context.assert(module, '内部用户停用状态正确', disableUserResponse, disabledUser?.status === 'DISABLED', `status=${disabledUser?.status ?? 'missing'}`)
await verifyRevokedSessions(context, '账号停用撤销会话', [activeLogin])
const disabledLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: resetPassword }, '')
context.addStep(module, '停用用户禁止登录', disabledLoginResponse,
disabledLoginResponse.http === 200 && String(disabledLoginResponse.body?.code) !== '200',
`HTTP=${disabledLoginResponse.http}, code=${disabledLoginResponse.body?.code ?? 'missing'}`)
const deleteRoleResponse = await context.request('DELETE', `/api/platform/v1/iam/roles/${roleId}`)
context.expectConflict(module, '已分配成员的角色禁止删除', deleteRoleResponse, 'system-role-protection')
const finalDisableRoleResponse = await context.request('PUT', `/api/platform/v1/iam/roles/${roleId}/status`, { status: 'DISABLED' })
const finalRole = context.apiSuccess(module, '清理时停用测试角色', finalDisableRoleResponse)
context.assert(module, '保留测试角色处于停用状态', finalDisableRoleResponse, finalRole?.status === 'DISABLED',
`status=${finalRole?.status ?? 'missing'}`)
const deletePermissionResponse = await context.request('DELETE', `/api/platform/v1/iam/permissions/${permissionId}`)
context.apiSuccess(module, '删除临时权限资源', deletePermissionResponse)
for (const additionalPermissionId of additionalPermissionIds) {
const response = await context.request('DELETE', `/api/platform/v1/iam/permissions/${additionalPermissionId}`)
context.apiSuccess(module, '删除四类型验收权限资源', response, `permissionId=${additionalPermissionId}`)
}
}
function requiredUserFields() {
return ['membershipId', 'userId', 'authUserId', 'username', 'displayName', 'phone', 'email',
'tenantId', 'tenantName', 'memberType', 'status', 'roleIds', 'roleNames', 'protectedAccount']
}