diff --git a/scripts/admin-test-env-modules/phase4-overseas-master-data-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-master-data-contract.test.mjs index 7ffc77066..a83d35208 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-master-data-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-master-data-contract.test.mjs @@ -4,16 +4,10 @@ import test from 'node:test' import { run } from './phase4-overseas-master-data.mjs' const source = readFileSync(new URL('./phase4-overseas-master-data.mjs', import.meta.url), 'utf8') -const controllerSource = [ - '../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteController.java', - '../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateController.java', - '../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/controller/PlatformVehicleController.java', -].map((path) => readFileSync(new URL(path, import.meta.url), 'utf8')).join('\n') const DOMESTIC_BASE = '/api/platform/v1/warehouse-admin' const OVERSEAS_BASE = '/api/platform/v1/last-leg/master-data' const ok = (data) => ({ http: 200, body: { code: '200', message: 'success', data }, requestId: 'req-p4', traceId: 'trace-p4' }) const conflict = () => ({ http: 200, body: { code: 'A0409', message: 'record version conflict', data: null }, requestId: 'req-conflict' }) -const forbidden = () => ({ http: 403, body: { code: 'A0403', message: 'Access denied', data: null }, requestId: 'req-forbidden' }) function detailQuote(state) { return { @@ -28,25 +22,21 @@ function vehicleWire(state) { return { ...state.vehicle, mileageKm: String(state.vehicle.mileageKm), recordVersion: String(state.vehicle.recordVersion) } } -function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) { +function fakeContext() { const calls = [] const coverageGaps = [] - const steps = [] const marker = 'ADMIN-API-20260821123456789' - const state = { warehouse: null, quote: null, exchange: null, vehicle: null, optionReads: 0, role: null, user: null } + const state = { warehouse: null, quote: null, exchange: null, vehicle: null } const context = { - marker, calls, coverageGaps, steps, createdResources: {}, - async findAvailablePhone() { return '13800138000' }, + marker, calls, coverageGaps, createdResources: {}, apiSuccess(_module, name, response) { assert.equal(response?.http, 200, `${name}: HTTP`) assert.equal(String(response?.body?.code), '200', `${name}: code`) return response.body.data }, - assert(_module, name, _response, condition, evidence, options = {}) { - const passed = Boolean(condition) - steps.push({ name, passed, deploymentBlocker: Boolean(options.deploymentBlocker && !passed), evidence }) - if (!passed && !softAssertions) assert.equal(passed, true, `${name}: ${evidence}`) - return passed + assert(_module, name, _response, condition, evidence) { + assert.equal(Boolean(condition), true, `${name}: ${evidence}`) + return true }, expectConflict(_module, name, response) { assert.equal(response.http, 200, `${name}: HTTP`) @@ -74,40 +64,12 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) { state.warehouse.status = 'INACTIVE' return ok(null) } - if (method === 'POST' && path === '/api/platform/v1/iam/roles') { - state.role = { id: '940000000000000001', ...body, status: 'ACTIVE' } - return ok({ ...state.role }) - } - if (method === 'POST' && path === '/api/platform/v1/iam/users') { - state.user = { membershipId: '950000000000000001', ...body, status: 'ACTIVE' } - return ok({ ...state.user }) - } - if (method === 'POST' && path === '/api/auth/login') { - return ok({ accessToken: 'deny-domestic-token', overseasAccessToken: 'deny-overseas-token', overseasExpiresIn: 900 }) - } - if (method === 'PUT' && path === '/api/platform/v1/iam/users/950000000000000001/status') { - state.user.status = body.status - return ok(null) - } - if (method === 'PUT' && path === '/api/platform/v1/iam/roles/940000000000000001/status') { - state.role.status = body.status - return ok(null) - } assert.fail(`unexpected domestic call ${method} ${path}`) }, - async requestOverseas(method, path, body, extraHeaders = {}, token = 'admin-overseas-token') { - calls.push({ gateway: 'overseas', method, path, body, extraHeaders, tokenKind: token === 'deny-overseas-token' ? 'denied' : 'admin' }) + async requestOverseas(method, path, body) { + calls.push({ gateway: 'overseas', method, path, body }) assert.notEqual(method, 'DELETE') - if (token === 'deny-overseas-token') return forbidden() if (method === 'GET' && path === `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`) { - state.optionReads += 1 - if (!state.warehouse) return ok([]) - return ok([{ warehouseId: '910000000000000001', warehouseCode: state.warehouse.warehouseCode, - warehouseName: state.warehouse.warehouseName, countryCode: 'US' }]) - } - if (method === 'GET' && path === `${OVERSEAS_BASE}/overseas-quotes/warehouse-options?refresh=true`) { - state.optionReads += 1 - if (refreshEmpty) return ok([]) return ok([{ warehouseId: '910000000000000001', warehouseCode: state.warehouse.warehouseCode, warehouseName: state.warehouse.warehouseName, countryCode: 'US' }]) } @@ -119,10 +81,6 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) { } return ok(detailQuote(state.quote)) } - if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/overseas-quotes?page=1&size=20&keyword=`)) { - return ok({ records: state.quote ? [{ ...state.quote.summary }] : [], page: 1, size: 20, - total: state.quote ? 1 : 0, pages: state.quote ? 1 : 0 }) - } if (path === `${OVERSEAS_BASE}/overseas-quotes/920000000000000001`) { if (method === 'GET') return ok(detailQuote(state.quote)) if (method === 'PUT') { @@ -142,10 +100,7 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) { return ok(detailQuote(state.quote)) } if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=`)) { - const matches = state.exchange && path.includes(`monthFrom=${state.exchange.month}`) - && (!path.includes('status=') || path.includes(`status=${state.exchange.status}`)) - return ok({ records: matches ? [{ ...state.exchange }] : [], page: 1, size: 20, - total: matches ? 1 : 0, pages: matches ? 1 : 0 }) + return ok({ records: [], page: 1, size: 20, total: 0, pages: 0 }) } if (method === 'POST' && path === `${OVERSEAS_BASE}/exchange-rates`) { state.exchange = { ...body, baseCurrency: 'USD', quoteCurrency: 'CNY', status: 'ACTIVE', recordVersion: 0 } @@ -184,21 +139,13 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) { state.vehicle = { ...state.vehicle, status: body.status, recordVersion: state.vehicle.recordVersion + 1 } return ok(vehicleWire(state)) } - if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/vehicles?page=1&size=20&keyword=`)) { - return ok({ records: state.vehicle ? [vehicleWire(state)] : [], page: 1, size: 20, - total: state.vehicle ? 1 : 0, pages: state.vehicle ? 1 : 0 }) - } assert.fail(`unexpected overseas call ${method} ${path}`) }, async requestOverseasBinary(method, path) { calls.push({ gateway: 'overseas-binary', method, path }) assert.equal(method, 'GET') assert.match(path, /\/(?:overseas-quotes|exchange-rates|vehicles)\/export\?/) - let csv = '' - if (path.includes('/overseas-quotes/')) csv = `报价ID,报价名称,合作仓编码,合作仓名称,报价方,币种,生效开始,生效结束,版本号,状态,更新时间\r\n${state.quote.summary.id},${state.quote.summary.name},${state.quote.summary.warehouseCode},${state.quote.summary.warehouseName},${state.quote.summary.providerName},USD,2098-01-01,2098-12-31,V1,ACTIVE,2026-08-21\r\n` - else if (path.includes('/exchange-rates/')) csv = `月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间\r\n${state.exchange.month},USD,CNY,${state.exchange.usdToCnyAverage},${state.exchange.status},admin,2026-08-21\r\n` - else csv = `车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间\r\n${state.vehicle.id},${state.vehicle.plateNumber},${state.vehicle.model},18.75,101,Carrier,123,OVERSEAS,OWNED,${state.vehicle.status},2026-08-21\r\n` - return { method, path, http: 200, body: null, bytes: new TextEncoder().encode(`\uFEFF${csv}`), + return { method, path, http: 200, body: null, bytes: new TextEncoder().encode('id,status\n1,ACTIVE\n'), contentType: 'text/csv;charset=UTF-8', requestId: 'req-csv' } }, } @@ -213,20 +160,13 @@ test('静态锁定真实双Gateway生命周期、CSV与安全边界', () => { assert.match(source, /context\.requestOverseas\('PATCH'/) assert.match(source, /context\.requestOverseasBinary\('GET'/) assert.match(source, /context\.expectConflict/) - assert.match(source, /warehouse-options\?refresh=true/) - assert.match(source, /空候选不负缓存30分钟/) - assert.match(source, /30分钟正缓存与刷新快照一致/) - assert.match(source, /无quote:list权限访问海外报价被拒绝/) - assert.match(source, /表头与marker内容/) assert.match(source, /finally\s*\{/) assert.match(source, /context\.createdResources\.phase4OverseasMasterData/) - assert.doesNotMatch(source, /context\.addCoverageGap/) + assert.match(source, /context\.addCoverageGap/) assert.match(source, /2090 \+ Math\.floor/) assert.doesNotMatch(source, /\/internal\/|overseas-partners|https?:\/\/|fetch\(|process\.env|request(?:Overseas)?\('DELETE'/) for (const endpoint of ['overseas-quotes/warehouse-options', 'overseas-quotes/export', 'exchange-rates/export', 'vehicles/export']) assert.ok(source.includes(endpoint)) - assert.doesNotMatch(controllerSource, /Idempotency-Key|IdempotencyKey/) - assert.doesNotMatch(source, /Idempotency-Key/) }) test('marker资源完成创建、详情、编辑、冲突、状态、CSV并全部停用', async () => { @@ -246,39 +186,30 @@ test('marker资源完成创建、详情、编辑、冲突、状态、CSV并全 assert.equal(state.quote.summary.status, 'INACTIVE') assert.equal(state.exchange.status, 'INACTIVE') assert.equal(state.vehicle.status, 'INACTIVE') - assert.equal(state.role.status, 'DISABLED') - assert.equal(state.user.status, 'DISABLED') - assert.equal(state.optionReads, 4) assert.equal(context.calls.filter((call) => call.gateway === 'overseas-binary').length, 3) assert.equal(context.calls.filter((call) => call.method === 'DELETE').length, 0) - assert.equal(context.calls.filter((call) => call.method === 'PUT').length, 9) - assert.equal(context.calls.filter((call) => call.tokenKind === 'denied').length, 1) - assert.equal(context.coverageGaps.length, 0) + assert.equal(context.calls.filter((call) => call.method === 'PUT').length, 7) }) -test('创建前空候选不形成假PASS,刷新后本轮合作仓进入报价完整生命周期', async () => { - const { context } = fakeContext() +test('候选空缓存登记覆盖缺口并继续汇率车辆,finally停用国内合作仓', async () => { + const { context, state } = fakeContext() + const original = context.requestOverseas + context.requestOverseas = async (method, path, body) => { + if (method === 'GET' && path.endsWith('/overseas-quotes/warehouse-options')) return ok([]) + return original(method, path, body) + } await run(context) - const optionCalls = context.calls.filter((call) => call.path.includes('warehouse-options')) - assert.equal(optionCalls.length, 4) - assert.equal(optionCalls[0].path.endsWith('warehouse-options'), true) - assert.equal(optionCalls[1].path.endsWith('warehouse-options'), true) - assert.equal(optionCalls[2].path.endsWith('warehouse-options?refresh=true'), true) - assert.equal(optionCalls[3].path.endsWith('warehouse-options'), true) - assert.equal(context.createdResources.phase4OverseasMasterData.quote.id, '920000000000000001') - assert.equal(context.coverageGaps.length, 0) -}) - -test('refresh后仍为空时报价前置明确deployment blocker且不产生coverageGap假PASS', async () => { - const { context, state } = fakeContext({ softAssertions: true, refreshEmpty: true }) - await run(context) - assert.equal(state.quote, null) assert.equal(state.warehouse.status, 'INACTIVE') + assert.equal(state.quote, null) assert.equal(state.exchange.status, 'INACTIVE') assert.equal(state.vehicle.status, 'INACTIVE') - assert.equal(context.coverageGaps.length, 0) - assert.equal(context.steps.some((step) => step.name === '刷新后新建合作仓立即可见' - && !step.passed && step.deploymentBlocker), true) - assert.equal(context.steps.some((step) => step.name === '海外报价生命周期执行' - && !step.passed && step.deploymentBlocker), true) + assert.equal(context.createdResources.phase4OverseasMasterData.quote.skipped, true) + assert.equal(context.createdResources.phase4OverseasMasterData.partnerWarehouse.finalStatus, 'INACTIVE') + assert.equal(context.createdResources.phase4OverseasMasterData.exchangeRate.finalStatus, 'INACTIVE') + assert.equal(context.createdResources.phase4OverseasMasterData.vehicle.finalStatus, 'INACTIVE') + assert.equal(context.coverageGaps.length, 1) + assert.equal(context.coverageGaps[0].name, '海外报价完整写生命周期') + assert.equal(context.coverageGaps[0].category, 'deployment-prerequisite') + assert.match(context.coverageGaps[0].reason, /30分钟空缓存/) + assert.equal(context.calls.filter((call) => call.gateway === 'overseas-binary').length, 2) }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs b/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs index 3d8c8f90e..8e181bae6 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs @@ -14,26 +14,13 @@ function requireData(context, name, response) { return value } -function assertBinaryCsv(context, name, response, expectedHeader, expectedTokens) { - const text = response?.bytes instanceof Uint8Array ? new TextDecoder().decode(response.bytes).replace(/^\uFEFF/, '') : '' - const header = text.split(/\r?\n/, 1)[0] +function assertBinaryCsv(context, name, response) { context.assert(MODULE, name, response, response?.http === 200 && String(response.contentType || '').toLowerCase().startsWith('text/csv') && response.bytes instanceof Uint8Array && response.bytes.length > 0, `HTTP=${response?.http ?? 0}, contentType=${response?.contentType || 'missing'}, bytes=${response?.bytes?.length || 0}`, BLOCKER) - context.assert(MODULE, `${name}表头与marker内容`, response, - header === expectedHeader && expectedTokens.every((token) => text.includes(String(token))), - `header=${header || 'missing'}, expectedTokens=${expectedTokens.join('|')}`, BLOCKER) -} - -const optionIds = (values) => Array.isArray(values) ? values.map((item) => asId(item?.warehouseId)).filter(Boolean) : [] - -function assertForbidden(context, name, response) { - context.assert(MODULE, name, response, - response?.http === 403 || (response?.http === 200 && ['403', 'A0403'].includes(String(response?.body?.code))), - `HTTP=${response?.http ?? 0}, code=${response?.body?.code ?? 'missing'}`, BLOCKER) } async function safeCleanup(context, name, action) { @@ -110,16 +97,6 @@ async function createAndVerifyPartnerWarehouse(context, resource) { BLOCKER) } -async function captureWarehouseOptionsBeforeCreate(context, resource) { - const response = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`) - const options = requireData(context, '创建合作仓前查询报价候选缓存基线', response) - context.assert(MODULE, '合作仓候选基线为规范数组', response, - Array.isArray(options) && options.every((item) => asId(item?.warehouseId) - && item.warehouseCode && item.warehouseName && /^[A-Z]{2}$/.test(item.countryCode)), - `count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER) - resource.partnerWarehouseOptionsBefore = optionIds(options) -} - function quoteCreatePayload(context, warehouseId) { return { name: `${context.marker} 海外报价`, warehouseId, @@ -150,25 +127,27 @@ async function quoteLifecycle(context, resource) { Array.isArray(options) && options.every((item) => asId(item.warehouseId) && item.warehouseCode && item.warehouseName && item.countryCode), `count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER) - const createdId = resource.partnerWarehouse?.id - if (resource.partnerWarehouseOptionsBefore?.length === 0) { - context.assert(MODULE, '空候选不负缓存30分钟', optionsResponse, - optionIds(options).includes(createdId), `createdId=${createdId}, ids=${optionIds(options).join(',') || 'empty'}`, BLOCKER) + if (Array.isArray(options) && options.length === 0) { + resource.quote = { + skipped: true, + reason: 'warehouse-options-empty-cache', + finalStatus: null, + } + context.addCoverageGap(MODULE, BLOCKER.scenario, '海外报价完整写生命周期', + 'warehouse-options 返回空数组;已创建的 OVERSEAS_PARTNER 可能尚未穿透服务端30分钟空缓存。本轮跳过报价且不猜测 warehouseId,继续验收汇率与车辆。', + 'deployment-prerequisite', { + marker: context.marker, + partnerWarehouseId: resource.partnerWarehouse?.id || null, + http: optionsResponse?.http ?? null, + code: optionsResponse?.body?.code ?? null, + requestId: optionsResponse?.requestId ?? null, + traceId: optionsResponse?.traceId ?? null, + }) + return false } - - const refreshResponse = await context.requestOverseas('GET', - `${OVERSEAS_BASE}/overseas-quotes/warehouse-options?refresh=true`) - const refreshed = requireData(context, '强制刷新海外报价合作仓候选', refreshResponse) - const option = Array.isArray(refreshed) ? refreshed.find((item) => asId(item.warehouseId) === createdId) : null - context.assert(MODULE, '刷新后新建合作仓立即可见', refreshResponse, Boolean(option), - `createdId=${createdId}, ids=${optionIds(refreshed).join(',') || 'empty'}`, BLOCKER) - if (!option) throw new Error(`warehouse-options refresh=true 未返回本轮合作仓 ${createdId}`) - - const cachedResponse = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`) - const cached = requireData(context, '刷新后读取30分钟正缓存', cachedResponse) - context.assert(MODULE, '30分钟正缓存与刷新快照一致', cachedResponse, - JSON.stringify(cached) === JSON.stringify(refreshed), - `refreshed=${optionIds(refreshed).join(',')}, cached=${optionIds(cached).join(',')}`, BLOCKER) + const createdOption = options.find((item) => asId(item.warehouseId) === resource.partnerWarehouse?.id) + const option = createdOption || options[0] + if (!option) throw new Error('海外报价接口未返回可用合作仓候选') const payload = quoteCreatePayload(context, asId(option.warehouseId)) const createResponse = await context.requestOverseas('POST', `${OVERSEAS_BASE}/overseas-quotes`, payload) @@ -185,13 +164,6 @@ async function quoteLifecycle(context, resource) { && Array.isArray(detail?.warehouseFees) && detail.warehouseFees.length > 0, `id=${asId(detail?.summary?.id) || 'missing'}, version=${detail?.summary?.recordVersion ?? 'missing'}`, BLOCKER) - const listResponse = await context.requestOverseas('GET', - `${OVERSEAS_BASE}/overseas-quotes?page=1&size=20&keyword=${encodeURIComponent(context.marker)}`) - const page = requireData(context, '分页查询marker海外报价', listResponse) - context.assert(MODULE, '报价列表包含本轮长ID与recordVersion', listResponse, - Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)), - `id=${id}, total=${page?.total ?? 'missing'}`, BLOCKER) - const staleVersion = Number(detail.summary.recordVersion) const updatePayload = quoteUpdatePayload({ ...payload, transitPrices: payload.transitPrices }, context, staleVersion) const updateResponse = await context.requestOverseas('PUT', path, updatePayload) @@ -219,9 +191,7 @@ async function quoteLifecycle(context, resource) { detail.summary.status === 'ACTIVE', `status=${detail.summary.status}`, BLOCKER) const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/overseas-quotes/export?keyword=${encodeURIComponent(context.marker)}`) - assertBinaryCsv(context, '导出海外报价CSV', csv, - '报价ID,报价名称,合作仓编码,合作仓名称,报价方,币种,生效开始,生效结束,版本号,状态,更新时间', - [id, context.marker]) + assertBinaryCsv(context, '导出海外报价CSV', csv) return true } @@ -282,17 +252,9 @@ async function exchangeLifecycle(context, resource) { context.assert(MODULE, '汇率状态切换为INACTIVE', statusResponse, rate.status === 'INACTIVE', `status=${rate.status}`, BLOCKER) - const listResponse = await context.requestOverseas('GET', - `${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}&status=INACTIVE`) - const page = requireData(context, '分页查询marker月汇率', listResponse) - context.assert(MODULE, '汇率列表包含本轮月份与recordVersion', listResponse, - Array.isArray(page?.records) && page.records.some((item) => item.month === month && recordVersion(item.recordVersion)), - `month=${month}, total=${page?.total ?? 'missing'}`, BLOCKER) - const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/exchange-rates/export?monthFrom=${month}&monthTo=${month}`) - assertBinaryCsv(context, '导出marker月汇率CSV', csv, - '月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间', [month, 'INACTIVE']) + assertBinaryCsv(context, '导出marker月汇率CSV', csv) } function vehiclePayload(context, edited = false) { @@ -354,48 +316,8 @@ async function vehicleLifecycle(context, resource) { context.assert(MODULE, '车辆状态切换为MAINTENANCE', statusResponse, vehicle.status === 'MAINTENANCE', `status=${vehicle.status}`, BLOCKER) - const listResponse = await context.requestOverseas('GET', - `${OVERSEAS_BASE}/vehicles?page=1&size=20&keyword=${encodeURIComponent(createPayload.plateNumber)}`) - const page = requireData(context, '分页查询marker车辆', listResponse) - context.assert(MODULE, '车辆列表包含本轮长ID与recordVersion', listResponse, - Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)), - `id=${id}, total=${page?.total ?? 'missing'}`, BLOCKER) - const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/vehicles/export?keyword=${encodeURIComponent(createPayload.plateNumber)}`) - assertBinaryCsv(context, '导出marker车辆CSV', csv, - '车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间', - [id, createPayload.plateNumber, 'MAINTENANCE']) -} - -async function verifyCanonicalPermissionDenial(context, resource) { - const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase() - const roleResponse = await context.request('POST', '/api/platform/v1/iam/roles', { - roleCode: `P4_MD_DENY_${suffix}`.slice(0, 64), roleName: `四期主数据拒绝-${suffix}`, description: context.marker, - }) - const role = requireData(context, '创建无主数据权限角色', roleResponse) - const roleId = asId(role?.id) - if (!roleId) throw new Error('无主数据权限角色未返回ID') - resource.permissionProbe = { roleId, membershipId: null, roleStatus: 'ACTIVE', userStatus: null } - - const phone = await context.findAvailablePhone(MODULE) - const username = `p4_md_deny_${suffix}`.slice(0, 64).toLowerCase() - const password = `P4md${suffix.slice(-8)}A1!` - const userResponse = await context.request('POST', '/api/platform/v1/iam/users', { - username, displayName: `四期主数据拒绝-${suffix}`, phone, - email: `${username}@example.com`, tenantId: '0', roleIds: [roleId], password, status: 'ACTIVE', - }) - const user = requireData(context, '创建无主数据权限用户', userResponse) - const membershipId = asId(user?.membershipId) - if (!membershipId) throw new Error('无主数据权限用户未返回membershipId') - resource.permissionProbe.membershipId = membershipId - resource.permissionProbe.userStatus = 'ACTIVE' - - const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '') - const login = requireData(context, '无主数据权限用户登录', loginResponse) - if (!login?.overseasAccessToken) throw new Error('无主数据权限用户未签发海外Token') - const denied = await context.requestOverseas('GET', - `${OVERSEAS_BASE}/overseas-quotes?page=1&size=1`, undefined, {}, login.overseasAccessToken) - assertForbidden(context, '无quote:list权限访问海外报价被拒绝', denied) + assertBinaryCsv(context, '导出marker车辆CSV', csv) } async function cleanupPartnerWarehouse(context, resource) { @@ -433,33 +355,16 @@ async function cleanupVersioned(context, resource, key, path, extract) { } } -async function cleanupPermissionProbe(context, resource) { - const probe = resource.permissionProbe - if (!probe) return - if (probe.membershipId && probe.userStatus !== 'DISABLED') { - const response = await context.request('PUT', `/api/platform/v1/iam/users/${probe.membershipId}/status`, { status: 'DISABLED' }) - context.apiSuccess(MODULE, '无权限探针用户finally停用', response, `membershipId=${probe.membershipId}`, BLOCKER) - if (success(response)) probe.userStatus = 'DISABLED' - } - if (probe.roleId && probe.roleStatus !== 'DISABLED') { - const response = await context.request('PUT', `/api/platform/v1/iam/roles/${probe.roleId}/status`, { status: 'DISABLED' }) - context.apiSuccess(MODULE, '无权限探针角色finally停用', response, `roleId=${probe.roleId}`, BLOCKER) - if (success(response)) probe.roleStatus = 'DISABLED' - } -} - export async function run(context) { const resource = { marker: context.marker } context.createdResources.phase4OverseasMasterData = resource try { await continueLifecycle(context, '海外报价', async () => { - await captureWarehouseOptionsBeforeCreate(context, resource) await createAndVerifyPartnerWarehouse(context, resource) await quoteLifecycle(context, resource) }) await continueLifecycle(context, '月度汇率', () => exchangeLifecycle(context, resource)) await continueLifecycle(context, '车辆主数据', () => vehicleLifecycle(context, resource)) - await continueLifecycle(context, 'canonical权限拒绝', () => verifyCanonicalPermissionDenial(context, resource)) } finally { await safeCleanup(context, '海外报价', async () => { if (!resource.quote?.id) return @@ -477,6 +382,5 @@ export async function run(context) { await cleanupVersioned(context, resource, 'vehicle', path, (value) => value) }) await safeCleanup(context, '合作仓', () => cleanupPartnerWarehouse(context, resource)) - await safeCleanup(context, 'canonical权限探针', () => cleanupPermissionProbe(context, resource)) } }