优化
This commit is contained in:
@@ -10,9 +10,10 @@ import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
const DEFAULT_GATEWAY = 'http://8.163.30.128:9000'
|
||||
const DEFAULT_OVERSEAS_GATEWAY = 'http://8.163.30.128:9300'
|
||||
const DEFAULT_USERNAME = 'platform_admin'
|
||||
const DEFAULT_PASSWORD = 'ChangeMe123!'
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-loading-finalize', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-loading-finalize', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
|
||||
export function normalizeModuleOrder(modules) {
|
||||
const normalized = [...modules]
|
||||
@@ -131,13 +132,14 @@ function usage() {
|
||||
|
||||
选项:
|
||||
--gateway <url> Gateway 地址,默认 ${DEFAULT_GATEWAY}
|
||||
--overseas-gateway <url> 海外 Gateway 地址,默认 ${DEFAULT_OVERSEAS_GATEWAY}
|
||||
--username <name> 管理员账号,默认 ${DEFAULT_USERNAME}
|
||||
--password <value> 管理员密码,默认使用上方测试密码
|
||||
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
|
||||
--output <path> JSON 报告路径,默认 reports/
|
||||
--help 显示帮助
|
||||
|
||||
环境变量覆盖: ADMIN_TEST_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD`)
|
||||
环境变量覆盖: ADMIN_TEST_GATEWAY, ADMIN_OVERSEAS_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD`)
|
||||
}
|
||||
|
||||
export function parseArguments(argv) {
|
||||
@@ -158,6 +160,7 @@ export function parseArguments(argv) {
|
||||
}
|
||||
return {
|
||||
gateway: (values.get('gateway') || process.env.ADMIN_TEST_GATEWAY || DEFAULT_GATEWAY).replace(/\/$/, ''),
|
||||
overseasGateway: (values.get('overseas-gateway') || process.env.ADMIN_OVERSEAS_GATEWAY || DEFAULT_OVERSEAS_GATEWAY).replace(/\/$/, ''),
|
||||
username: values.get('username') || process.env.ADMIN_TEST_USERNAME || DEFAULT_USERNAME,
|
||||
password: values.get('password') || process.env.ADMIN_TEST_PASSWORD || DEFAULT_PASSWORD,
|
||||
modules: normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean)),
|
||||
@@ -189,6 +192,7 @@ export class AcceptanceRun {
|
||||
this.coverageGaps = []
|
||||
this.context = {}
|
||||
this.token = ''
|
||||
this.overseasToken = ''
|
||||
this.fatal = null
|
||||
}
|
||||
|
||||
@@ -215,6 +219,29 @@ export class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
|
||||
async requestOverseas(method, path, body = undefined, extraHeaders = {}) {
|
||||
const headers = { Accept: 'application/json', ...extraHeaders }
|
||||
if (this.overseasToken) headers.Authorization = `Bearer ${this.overseasToken}`
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json; charset=utf-8'
|
||||
try {
|
||||
const response = await fetch(`${this.options.overseasGateway}${path}`, {
|
||||
method, headers, body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
})
|
||||
const content = await response.text()
|
||||
let payload = null
|
||||
try { payload = content ? JSON.parse(content) : null } catch { /* Raw response is recorded below. */ }
|
||||
return {
|
||||
method, path, http: response.status, body: payload,
|
||||
raw: payload === null ? content.slice(0, 500) : null,
|
||||
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
||||
traceId: payload?.traceId || response.headers.get('x-trace-id'),
|
||||
}
|
||||
} catch (error) {
|
||||
return { method, path, http: 0, body: null, raw: error instanceof Error ? error.message : String(error), requestId: null, traceId: null }
|
||||
}
|
||||
}
|
||||
|
||||
async requestMultipart(method, path, file, token = this.token, extraHeaders = {}) {
|
||||
const form = file instanceof FormData ? file : new FormData()
|
||||
if (!(file instanceof FormData)) {
|
||||
@@ -266,6 +293,31 @@ export class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
|
||||
async requestOverseasBinary(method, path) {
|
||||
const headers = { Accept: '*/*' }
|
||||
if (this.overseasToken) headers.Authorization = `Bearer ${this.overseasToken}`
|
||||
try {
|
||||
const response = await fetch(`${this.options.overseasGateway}${path}`, {
|
||||
method, headers, signal: AbortSignal.timeout(120_000),
|
||||
})
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
let payload = null
|
||||
if (contentType.toLowerCase().includes('json')) {
|
||||
try { payload = JSON.parse(new TextDecoder().decode(bytes)) } catch { /* Invalid envelope remains binary evidence. */ }
|
||||
}
|
||||
return {
|
||||
method, path, http: response.status, body: payload, bytes, contentType,
|
||||
raw: payload ? null : `${bytes.length} bytes (${contentType || 'unknown'})`,
|
||||
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
||||
traceId: payload?.traceId || response.headers.get('x-trace-id'),
|
||||
}
|
||||
} catch (error) {
|
||||
return { method, path, http: 0, body: null, bytes: new Uint8Array(), contentType: '',
|
||||
raw: error instanceof Error ? error.message : String(error), requestId: null, traceId: null }
|
||||
}
|
||||
}
|
||||
|
||||
addStep(module, name, response, passed, evidence = '', options = {}) {
|
||||
const step = {
|
||||
module, scenario: options.scenario || null, name, passed: Boolean(passed),
|
||||
@@ -792,6 +844,14 @@ export class AcceptanceRun {
|
||||
if (!login?.accessToken) this.fatal = '平台管理员登录失败,未执行业务模块'
|
||||
else {
|
||||
this.token = login.accessToken
|
||||
this.overseasToken = login.overseasAccessToken || ''
|
||||
const requiresOverseas = this.options.modules.some((module) => module.startsWith('phase4-overseas-'))
|
||||
if (requiresOverseas) {
|
||||
this.assert('authentication', '登录响应包含有效海外访问令牌', loginResponse,
|
||||
Boolean(this.overseasToken) && Number(login.overseasExpiresIn) > 0,
|
||||
`tokenPresent=${Boolean(this.overseasToken)}, expiresIn=${login.overseasExpiresIn ?? 'missing'}`,
|
||||
{ deploymentBlocker: true, scenario: 'phase4-dual-token' })
|
||||
}
|
||||
const handlers = { customers: () => this.runCustomers() }
|
||||
for (let moduleIndex = 0; moduleIndex < this.options.modules.length; moduleIndex += 1) {
|
||||
const module = this.options.modules[moduleIndex]
|
||||
@@ -856,7 +916,7 @@ export class AcceptanceRun {
|
||||
deploymentBlockers: this.deploymentBlockers, coverageGaps: this.coverageGaps,
|
||||
createdResources: this.createdResources,
|
||||
})
|
||||
const forbiddenValues = [this.options.password, this.token].filter((value) => typeof value === 'string' && value.length >= 8)
|
||||
const forbiddenValues = [this.options.password, this.token, this.overseasToken].filter((value) => typeof value === 'string' && value.length >= 8)
|
||||
const sensitivePatterns = [
|
||||
/"(?:password|newPassword|temporaryPassword|accessToken|refreshToken|authorization)"\s*:/i,
|
||||
/\bBearer\s+[A-Za-z0-9._~-]{16,}/i,
|
||||
@@ -872,7 +932,8 @@ export class AcceptanceRun {
|
||||
const failed = this.steps.filter((step) => !step.passed).length
|
||||
return {
|
||||
reportName: '后台管理测试环境 HTTP API 验收', marker: this.marker,
|
||||
gateway: this.options.gateway, generatedAt: new Date().toISOString(), startedAt: this.startedAt,
|
||||
gateway: this.options.gateway, overseasGateway: this.options.overseasGateway,
|
||||
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
|
||||
success: this.fatal === null && failed === 0, fatal: this.fatal,
|
||||
summary: { total: this.steps.length, passed: this.steps.length - failed, failed },
|
||||
modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers,
|
||||
|
||||
Reference in New Issue
Block a user