Files
order_site/apps/backend/scripts/gen-test-cases.ts
T

427 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生成测试用例文档脚本
* 运行所有场景,输出请求/响应报文用于填写 测试用例.md
*
* 用法:
* npm run test:industry:gen -- --baseUrl=http://127.0.0.1
*/
import crypto from 'node:crypto'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { loadEnvFiles } from '../src/config/runtime-env.js'
type JsonObject = Record<string, unknown>
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const args = parseArgs(process.argv.slice(2))
const baseUrl = String(args.baseUrl || 'http://127.0.0.1').replace(/\/+$/, '')
const appKey = String(args.appKey || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = String(args.signSecret || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim()
const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
if (!appKey || !signSecret) {
console.error('请配置 appKey 和 signSecret')
process.exit(1)
}
const oid = `TEST_CASE_${Date.now()}`
console.log('# 测试用例生成\n')
console.log(`> 服务地址: ${baseUrl}`)
console.log(`> 测试订单号: ${oid}`)
console.log(`> appKey: ${appKey}`)
console.log(`> signMethod: ${signMethod}\n`)
// ───── helpers ─────
function buildParams(biz: JsonObject, method = signMethod): JsonObject {
const paramStr = JSON.stringify(biz)
const p: JsonObject = {
appkey: appKey,
version: '1',
timestamp: Date.now(),
signMethod: method,
access_token: '',
method: '',
param: paramStr,
}
p.sign = signKuaishou(p, method)
return p
}
function buildParamsFakeSign(biz: JsonObject): JsonObject {
const p = buildParams(biz)
p.sign = '00000000000000000000000000000000'
return p
}
function buildParamsNoAppKey(biz: JsonObject): JsonObject {
const paramStr = JSON.stringify(biz)
const p: JsonObject = {
version: '1',
timestamp: Date.now(),
signMethod: signMethod,
access_token: '',
method: '',
param: paramStr,
}
p.sign = signKuaishou(p, signMethod)
return p
}
function signKuaishou(params: JsonObject, method: string): string {
const entries = Object.entries(params)
.filter(([k]) => k !== 'sign' && k !== 'signSecret')
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const qs = entries.map(([k, v]) => `${k}=${stringifyVal(v)}`).join('&')
const src = `${qs}&signSecret=${signSecret}`
if (method === 'HMAC_SHA256')
return crypto.createHmac('sha256', signSecret).update(src, 'utf8').digest('base64')
return crypto.createHash('md5').update(src, 'utf8').digest('hex').toLowerCase()
}
function stringifyVal(v: unknown): string {
if (typeof v === 'number' && Number.isFinite(v)) return String(v)
if (typeof v === 'boolean') return v ? 'true' : 'false'
if (v == null) return ''
if (typeof v === 'object') return JSON.stringify(v, Object.keys(v as JsonObject).sort())
return String(v)
}
async function call(endpoint: string, params: JsonObject) {
const url = `${baseUrl}/api/v1/open/kuaishou-industry${endpoint}`
const body = new URLSearchParams()
for (const [k, v] of Object.entries(params)) {
if (k !== 'param') body.append(k, String(v))
}
body.append('param', String(params.param || '{}'))
const started = Date.now()
let status = 0
let json: JsonObject = {}
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
status = res.status
const text = await res.text()
try {
json = JSON.parse(text)
} catch {
json = { raw: text }
}
} catch (err) {
json = { error: String(err) }
}
return { status, json, ms: Date.now() - started }
}
function paramsToForm(params: JsonObject): string {
const p: string[] = []
for (const [k, v] of Object.entries(params)) {
if (k !== 'param' && k !== 'sign') p.push(`${k}=${v}`)
}
const signPart = params.sign ? `&sign=${params.sign}` : ''
return p.join('&') + `&param=${encodeURIComponent(String(params.param || '{}'))}` + signPart
}
function pp(v: unknown): string {
return JSON.stringify(v, null, 2)
}
// ───── 先准备订单 ─────
console.log('---')
console.log('准备测试订单...\n')
const prepBiz: JsonObject = {
oid,
sellerId: '2174425348',
num: 3,
itemId: '20842040339391',
skuId: '87457167264391',
itemTitle: '测试电子凭证商品',
sendType: 'VIRTUAL',
eticketType: 'DINING_OPEN_TICKET',
token: `token-${oid}`,
certExpireType: 3,
certExpDays: 30,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}
const prepParams = buildParams(prepBiz)
const prep = await call('/send-code', prepParams)
console.log(` 发码结果: result=${prep.json.result} (${prep.ms}ms)\n`)
let firstTicketId = ''
if (prep.json.data?.etickets?.length > 0) {
firstTicketId = String(prep.json.data.etickets[0].id)
}
// ────────────────────────────────────────────
console.log('═══════════════════════════════════════════')
console.log('一、通知发码接口测试用例')
console.log('═══════════════════════════════════════════\n')
// 1.1 收单成功
console.log('【场景1】收单成功\n')
const sendBiz: JsonObject = {
oid: `SUCCESS_${Date.now()}`,
sellerId: '2174425348',
num: 2,
itemId: '20842040339391',
skuId: '87457167264391',
itemTitle: '测试电子凭证',
sendType: 'VIRTUAL',
eticketType: 'DINING_OPEN_TICKET',
token: `token-success-${Date.now()}`,
certExpireType: 1,
certStartTime: Date.now(),
certEndTime: Date.now() + 365 * 24 * 60 * 60 * 1000,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 365 * 24 * 60 * 60 * 1000,
}
const sendParams = buildParams(sendBiz)
const sendRes = await call('/send-code', sendParams)
console.log(`请求报文 (application/x-www-form-urlencoded):`)
console.log(` ${paramsToForm(sendParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(sendRes.json)}\n`)
console.log(`HTTP ${sendRes.status} (${sendRes.ms}ms)\n`)
// 1.2 拒单:appkey 未传入
console.log('【场景2】拒单:appkey 未传入\n')
const noAppKeyBiz: JsonObject = {
oid: `NOKEY_${Date.now()}`,
sellerId: '2174425348',
num: 1,
itemId: '20842040339391',
sendType: 'VIRTUAL',
token: `token-nokey-${Date.now()}`,
certExpireType: 3,
certExpDays: 30,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}
const noKeyParams = buildParamsNoAppKey(noAppKeyBiz)
const noKeyRes = await call('/send-code', noKeyParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(noKeyParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(noKeyRes.json)}\n`)
console.log(`HTTP ${noKeyRes.status} (${noKeyRes.ms}ms)\n`)
// 1.3 拒单:签名错误
console.log('【场景3】拒单:签名错误\n')
const badSignBiz: JsonObject = {
oid: `BADSIGN_${Date.now()}`,
sellerId: '2174425348',
num: 1,
itemId: '20842040339391',
sendType: 'VIRTUAL',
token: `token-badsign-${Date.now()}`,
certExpireType: 3,
certExpDays: 30,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}
const badSignParams = buildParamsFakeSign(badSignBiz)
const badSignRes = await call('/send-code', badSignParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(badSignParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(badSignRes.json)}\n`)
console.log(`HTTP ${badSignRes.status} (${badSignRes.ms}ms)\n`)
// 1.4 拒单:参数缺失
console.log('【场景4】拒单:参数缺失(缺少必要字段 oid)\n')
const missingParams = buildParams({ sellerId: '123', num: 1, sendType: 'VIRTUAL' })
const missingRes = await call('/send-code', missingParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(missingParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(missingRes.json)}\n`)
console.log(`HTTP ${missingRes.status} (${missingRes.ms}ms)\n`)
// ────────────────────────────────────────────
console.log('═══════════════════════════════════════════')
console.log('二、查询发码结果接口测试用例')
console.log('═══════════════════════════════════════════\n')
// 2.1 查询成功
console.log('【场景5】查询成功:返回卡券信息\n')
const queryBiz: JsonObject = { oid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }
const queryParams = buildParams(queryBiz)
const queryRes = await call('/query-code', queryParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(queryParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(queryRes.json)}\n`)
console.log(`HTTP ${queryRes.status} (${queryRes.ms}ms)\n`)
// 2.2 查询单个卡券
if (firstTicketId) {
console.log('【场景6】查询成功:查询单个卡券(指定 eticketId\n')
const singleBiz: JsonObject = { oid, eticketId: firstTicketId, sendType: 'VIRTUAL' }
const singleParams = buildParams(singleBiz)
const singleRes = await call('/query-code', singleParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(singleParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(singleRes.json)}\n`)
console.log(`HTTP ${singleRes.status} (${singleRes.ms}ms)\n`)
// 2.3 卡券不存在
console.log('【场景7】查询失败:卡券不存在(错误码 4012005\n')
const notFoundBiz: JsonObject = { oid, eticketId: '99999', sendType: 'VIRTUAL' }
const notFoundParams = buildParams(notFoundBiz)
const notFoundRes = await call('/query-code', notFoundParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(notFoundParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(notFoundRes.json)}\n`)
console.log(`HTTP ${notFoundRes.status} (${notFoundRes.ms}ms)\n`)
}
// 2.4 订单不存在
console.log('【场景8】查询失败:订单不存在(错误码 4012002\n')
const noOrderBiz: JsonObject = { oid: `NOT_EXIST_${Date.now()}`, sendType: 'VIRTUAL' }
const noOrderParams = buildParams(noOrderBiz)
const noOrderRes = await call('/query-code', noOrderParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(noOrderParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(noOrderRes.json)}\n`)
console.log(`HTTP ${noOrderRes.status} (${noOrderRes.ms}ms)\n`)
// ────────────────────────────────────────────
console.log('═══════════════════════════════════════════')
console.log('三、关单 / 销毁接口测试用例')
console.log('═══════════════════════════════════════════\n')
// 3.1 销毁指定卡券成功
if (firstTicketId) {
console.log('【场景9】销毁指定卡券成功\n')
const destroyBiz: JsonObject = {
oid,
reason: 'USER_APPLY_REFUND',
etickets: [{ id: firstTicketId, num: 1, goodsValue: 100 }],
}
const destroyParams = buildParams(destroyBiz)
const destroyRes = await call('/destroy-code', destroyParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(destroyParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(destroyRes.json)}\n`)
console.log(`HTTP ${destroyRes.status} (${destroyRes.ms}ms)\n`)
// 3.2 销毁后查询验证
console.log('【场景10】销毁后验证卡券状态为 DESTROYED\n')
const verifyBiz: JsonObject = { oid, eticketId: firstTicketId, sendType: 'VIRTUAL' }
const verifyParams = buildParams(verifyBiz)
const verifyRes = await call('/query-code', verifyParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(verifyParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(verifyRes.json)}\n`)
console.log(`HTTP ${verifyRes.status} (${verifyRes.ms}ms)\n`)
}
// 3.3 整单销毁
console.log('【场景11】整单销毁(不传 etickets,关闭全部剩余卡券)\n')
const destroyAllBiz: JsonObject = { oid, reason: 'ETICKET_EXPIRED' }
const destroyAllParams = buildParams(destroyAllBiz)
const destroyAllRes = await call('/destroy-code', destroyAllParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(destroyAllParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(destroyAllRes.json)}\n`)
console.log(`HTTP ${destroyAllRes.status} (${destroyAllRes.ms}ms)\n`)
// 3.4 销毁不存在的订单(应返回成功)
console.log('【场景12】销毁不存在的订单(应返回 result=1\n')
const destroyFakeBiz: JsonObject = { oid: `FAKE_DESTROY_${Date.now()}`, reason: 'ETICKET_EXPIRED' }
const destroyFakeParams = buildParams(destroyFakeBiz)
const destroyFakeRes = await call('/destroy-code', destroyFakeParams)
console.log(`请求报文:`)
console.log(` ${paramsToForm(destroyFakeParams)}\n`)
console.log(`响应报文:`)
console.log(` ${JSON.stringify(destroyFakeRes.json)}\n`)
console.log(`HTTP ${destroyFakeRes.status} (${destroyFakeRes.ms}ms)\n`)
// ────────────────────────────────────────────
console.log('═══════════════════════════════════════════')
console.log('四、发码回调接口(商家 → 快手)')
console.log('═══════════════════════════════════════════')
console.log(`
说明:该接口由商家调用快手开放平台。
接口: POST https://openapi.kwaixiaodian.com/integration/virtual/eticket/callback
测试方式:调用快手正式环境,返回"订单不存在"即为通过。
请求参数由商家根据实际业务提供。
`)
console.log(`请求参数(示例):`)
console.log(` oid: "2319200000001216"`)
console.log(` etickets: [{ "id": "1", "code": "", "num": 1, "status": "ISSUED" }]`)
console.log(` token: "token-xxx" (来自发码通知中的 token)\n`)
console.log('═══════════════════════════════════════════')
console.log('五、核销回调接口(商家 → 快手,可选)')
console.log('═══════════════════════════════════════════')
console.log(`
说明:该接口由商家调用快手开放平台。
接口: POST https://openapi.kwaixiaodian.com/integration/callback/virtual/eticket/consume
测试方式:调用快手正式环境,出现"订单不存在"即视为调试通过。
详见: docs/行业电子凭证/4.电子凭证核销回调接口.md
`)
console.log(`请求参数(示例):`)
console.log(` oid: "2319200000001216"`)
console.log(` etickets: [{ "id": "12313", "num": 1, "goodsValue": 10 }]`)
console.log(` status: "CONSUMED"`)
console.log(` consumeType: "consume"`)
console.log(` storeName: "测试门店"`)
console.log(` storeAddress: "测试地址"`)
console.log(` token: "token-xxx"\n`)
console.log('═══════════════════════════════════════════')
console.log('六、销毁回调接口(商家 → 快手,可选)')
console.log('═══════════════════════════════════════════')
console.log(`
说明:该接口由商家调用快手开放平台。
接口: POST https://openapi.kwaixiaodian.com/integration/callback/virtual/eticket/destroy
测试方式:调用快手正式环境,出现"订单不存在"即视为调试通过。
`)
console.log(`请求参数(示例):`)
console.log(` oid: "2319200000001216"`)
console.log(` etickets: [{ "id": "1", "num": 1, "goodsValue": 100 }]`)
console.log(` reason: "ETICKET_EXPIRED"`)
console.log(` token: "token-xxx"\n`)
console.log('═══════════════════════════════════════════')
console.log('完成。')
console.log('═══════════════════════════════════════════')
function parseArgs(rawArgs: string[]) {
const parsed: JsonObject = {}
for (const raw of rawArgs) {
const arg = String(raw || '').trim()
if (!arg) continue
const n = arg.startsWith('--') ? arg.slice(2) : arg
const eq = n.indexOf('=')
if (eq < 0) {
parsed[n] = true
continue
}
parsed[n.slice(0, eq).trim()] = n.slice(eq + 1).trim()
}
return parsed
}