const IAM = '/api/platform/v1/iam' const id = value => String(value ?? '') const same = (a, b) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort()) export const POOL_ACTOR_SLOTS = ['pool.sales.0', 'pool.sales.1', 'pool.dispatch.0', 'pool.dispatch.1', 'pool.fulfillment.0'] export const UI_ACTOR_SLOTS = ['pool-ui.SALES_A', 'pool-ui.SALES_B', 'pool-ui.MANAGER'] export const COST_ACTOR_SLOTS = ['fleet-cost.reader', 'fleet-cost.denied'] export function requireFixedActors(options, slots) { const actors = options?.fixedActors const users = new Set(), memberships = new Set() for (const slot of slots) { const actor = actors?.[slot] if (!actor || !['membershipId', 'roleId'].every(key => typeof actor[key] === 'string' && /^[1-9]\d*$/.test(actor[key])) || !['username', 'password', 'roleCode'].every(key => typeof actor[key] === 'string' && actor[key].trim())) throw Error(`固定测试账号 ${slot} 配置不完整;禁止自动创建角色或账号`) if (users.has(actor.username) || memberships.has(actor.membershipId)) throw Error('隔离场景必须使用不同的固定账号,不能以同一账号代替') users.add(actor.username); memberships.add(actor.membershipId) } return actors } /** Only validates existing platform identities and opens/closes its own sessions. Never changes roles or accounts. */ export async function openFixedActor(context, slot, expectedCodes) { const actor = requireFixedActors(context.options, [slot])[slot] async function request(method, path, body, token = context.token) { const response = await context.request(method, path, body, token) if (response?.http !== 200 || String(response.body?.code) !== '200') throw Error(`固定账号 ${slot} 核验失败:${method} ${path.split('?')[0]}`) return response.body.data } const roles = await request('GET', `${IAM}/roles`) const matches = Array.isArray(roles) ? roles.filter(row => id(row.id) === actor.roleId) : [] if (matches.length !== 1 || matches[0].roleCode !== actor.roleCode || matches[0].status !== 'ACTIVE') throw Error(`固定账号 ${slot} 的角色身份或启用状态不匹配,不自动改角色`) let user, total for (let page = 1; page <= 100; page++) { // IAM keyword filters profile fields/member numbers, not the login name. Match the configured identity in the authoritative directory. const data = await request('GET', `${IAM}/users?${new URLSearchParams({ page: String(page), size: '100' })}`) if (!Array.isArray(data?.records) || Number(data.page) !== page || Number(data.size) !== 100 || !Number.isSafeInteger(Number(data.total)) || Number(data.total) < 0 || data.records.length > 100 || (total !== undefined && total !== Number(data.total))) throw Error('固定账号查询分页不完整或发生变化') total ??= Number(data.total) const candidates = data.records.filter(row => id(row.membershipId) === actor.membershipId && row.username === actor.username) if (candidates.length > 1) throw Error('固定账号查询身份重复') if (candidates.length) { user = candidates[0]; break } if (page * 100 >= total) break if (!data.records.length) throw Error('固定账号查询分页缺页') } if (!user || user.status !== 'ACTIVE' || id(user.tenantId) !== '0' || !Array.isArray(user.roleIds) || !same(user.roleIds.map(id), [actor.roleId])) throw Error(`固定账号 ${slot} 的成员、平台归属、状态或角色绑定不匹配,不自动修改账号`) const session = await request('POST', '/api/auth/login', { loginId: actor.username, password: actor.password }, '') let closed = false async function close() { if (closed) return if (!session?.accessToken || !session.refreshToken) throw Error('固定账号会话不完整,无法确认退出') const response = await context.request('POST', '/api/auth/logout', { refreshToken: session.refreshToken }, session.accessToken) if (!(response?.http === 200 && String(response.body?.code) === '200') && !([200, 401].includes(response?.http) && String(response.body?.code) === 'A0401')) throw Error('固定账号退出未确认,保留现场') closed = true } try { if (!session?.accessToken || !session.refreshToken || session.accessToken === context.token) throw Error('固定账号缺少独立完整会话') const codes = await request('GET', `${IAM}/authorization/permissions`, undefined, session.accessToken) if (!Array.isArray(codes) || !same(codes, expectedCodes)) throw Error(`固定账号 ${slot} 实际权限不匹配,不自动授权或扩大权限`) } catch (error) { try { await close() } catch (cleanupError) { throw new AggregateError([error, cleanupError], '固定账号核验和退出均失败') } throw error } return { membershipId: actor.membershipId, username: actor.username, roleId: actor.roleId, roleCode: actor.roleCode, token: session.accessToken, refreshToken: session.refreshToken, reused: true, close } }