Files
dukang/scripts/smoke-v3.mjs
T

286 lines
10 KiB
JavaScript
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.
import { loginWithSms } from './sms-test-helper.mjs';
const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1';
async function req(clientApp, path, options = {}) {
const headers = {
'Content-Type': 'application/json',
'X-Client-App': clientApp,
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
};
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
const json = await res.json();
if (json.code !== 0) throw new Error(`${path}: ${json.message}`);
return json.data;
}
async function expectFail(clientApp, path, options = {}) {
const headers = {
'Content-Type': 'application/json',
'X-Client-App': clientApp,
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
};
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
const json = await res.json();
if (json.code === 0) throw new Error(`${path}: expected failure`);
return json.message;
}
async function sendSms(clientApp, phone, scene, sendPath = '/auth/sms/send') {
await req(clientApp, sendPath, {
method: 'POST',
body: JSON.stringify({ phone, scene }),
});
}
async function loginSms(clientApp, phone, scene, loginPath = '/auth/login/sms', sendPath = '/auth/sms/send') {
return loginWithSms(clientApp, phone, scene, loginPath, sendPath);
}
async function adminLogin() {
return loginSms(
'HQ_WEB',
'13600000001',
'HQ_LOGIN',
'/admin/auth/login/sms',
'/admin/auth/sms/send',
);
}
async function main() {
console.log('1. Health');
await req('USER_H5', '/health');
console.log('2. User login');
const userLogin = await loginSms('USER_H5', '13800000001', 'USER_LOGIN');
const userToken = userLogin.accessToken;
console.log('3. Cities + products');
const cities = await req('USER_H5', '/catalog/cities');
const cityCode = cities[0]?.code ?? 'ZZ';
const products = await req('USER_H5', `/catalog/products?cityCode=${cityCode}`);
if (products.length < 4) throw new Error(`Expected >=4 products, got ${products.length}`);
console.log('4. Min purchase boundaries');
const addr = await req('USER_H5', '/user/addresses', {
method: 'POST',
token: userToken,
body: JSON.stringify({
receiverName: '测试',
phone: '13800000001',
province: '河南省',
city: '郑州市',
district: '金水区',
detail: 'V3冒烟地址',
isDefault: true,
}),
});
await expectFail('USER_H5', '/trade/orders/preview', {
method: 'POST',
token: userToken,
body: JSON.stringify({ productId: products[0].id, quantity: 1, addressId: addr.id }),
});
const preview2 = await req('USER_H5', '/trade/orders/preview', {
method: 'POST',
token: userToken,
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
});
if (preview2.payAmount <= 0) throw new Error('Preview qty=2 failed');
console.log('5. Create order + pay + benefit');
const order = await req('USER_H5', '/trade/orders', {
method: 'POST',
token: userToken,
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
});
await req('USER_H5', `/trade/orders/${order.id}/pay`, { method: 'POST', token: userToken });
const paidOrder = await req('USER_H5', `/trade/orders/${order.id}`, { token: userToken });
if (paidOrder.status !== 'PENDING_SHIP') throw new Error('Order not PENDING_SHIP after pay');
const coupons = await req('USER_H5', '/benefit/coupons', { token: userToken });
if (!coupons.length) throw new Error('No coupon after pay');
const summary = await req('USER_H5', '/benefit/summary', { token: userToken });
console.log('6. Direct redeem (no couponId)');
const directAmount = Math.min(100, Number(summary.totalBalance ?? summary.balance ?? 100));
const directToken = await req('USER_H5', '/redeem/tokens', {
method: 'POST',
token: userToken,
body: JSON.stringify({ amount: directAmount }),
});
if (!directToken.token) throw new Error('Direct redeem token missing');
console.log('7. Coupon redeem cap');
await expectFail('USER_H5', '/redeem/tokens', {
method: 'POST',
token: userToken,
body: JSON.stringify({ couponId: coupons[0].id, amount: Number(coupons[0].balance) + 1 }),
});
console.log('7b. Shop sms guard + session');
const unboundMsg = await expectFail('SHOP_H5', '/shop/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone: '13899999999', scene: 'STORE_LOGIN' }),
});
if (!unboundMsg.includes('未绑定门店')) {
throw new Error(`Expected unbound store message, got: ${unboundMsg}`);
}
console.log('8. Shop confirm redeem');
const shopLogin = await loginSms(
'SHOP_H5',
'13910000001',
'STORE_LOGIN',
'/shop/auth/login/sms',
'/shop/auth/sms/send',
);
let shopToken = shopLogin.accessToken;
let shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
if (!shopMe?.phone) throw new Error('Shop /shop/auth/me failed');
const refreshed = await req('SHOP_H5', '/shop/auth/token/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken: shopLogin.refreshToken }),
});
if (!refreshed?.accessToken) throw new Error('Shop token refresh failed');
shopToken = refreshed.accessToken;
shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
if (!shopMe.storeId && shopMe.stores?.[0]?.storeId) {
const selected = await req('SHOP_H5', '/shop/auth/select-store', {
method: 'POST',
token: shopToken,
body: JSON.stringify({ storeId: shopMe.stores[0].storeId }),
});
shopToken = selected.accessToken || shopToken;
shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
}
if (!shopMe.storeId) throw new Error('Shop storeId missing after select-store');
const preview = await req('SHOP_H5', '/shop/redeem/preview', {
method: 'POST',
token: shopToken,
body: JSON.stringify({ token: directToken.token }),
});
if (!preview.amount) throw new Error('Redeem preview failed');
await req('SHOP_H5', '/shop/redeem/confirm', {
method: 'POST',
token: shopToken,
body: JSON.stringify({ token: directToken.token }),
});
console.log('9. Store withdraw (OPT-010)');
const admin = await adminLogin();
const shopStoreId = shopMe.storeId || shopMe.stores?.[0]?.storeId;
if (!shopStoreId) throw new Error('Shop storeId missing after login');
const withdrawSummary = await req('SHOP_H5', '/shop/withdraw/summary', {
token: shopToken,
});
if (!(withdrawSummary.availableAmount > 0)) {
throw new Error('Expected available withdraw amount after redeem');
}
const applied = await req('SHOP_H5', '/shop/withdraw', {
method: 'POST',
token: shopToken,
body: JSON.stringify({}),
});
if (applied.status !== 'PENDING_REVIEW') {
throw new Error(`Expected PENDING_REVIEW withdraw, got ${applied.status}`);
}
const pendingDup = await expectFail('SHOP_H5', '/shop/withdraw', {
method: 'POST',
token: shopToken,
body: JSON.stringify({}),
});
if (!String(pendingDup).includes('待审核')) {
throw new Error(`Expected pending-request reject, got: ${pendingDup}`);
}
// 锁定中的明细不可再出账(T+1 排除 withdrawItem
const summaryAfter = await req('SHOP_H5', '/shop/withdraw/summary', {
token: shopToken,
});
if (summaryAfter.availableAmount !== 0) {
throw new Error('Expected availableAmount=0 while withdraw pending');
}
await req('HQ_WEB', `/admin/store-withdrawals/${applied.id}/approve`, {
method: 'POST',
token: admin.accessToken,
body: JSON.stringify({ paymentRef: 'smoke-withdraw' }),
});
const withdrawList = await req('SHOP_H5', '/shop/withdraw/requests?status=PAID', {
token: shopToken,
});
const settled = withdrawList.items?.find((w) => w.id === applied.id);
if (!settled || settled.status !== 'PAID') {
throw new Error('Expected settled withdraw request on shop side');
}
const overdue = await req('HQ_WEB', '/admin/store-withdrawals/overdue-summary', {
token: admin.accessToken,
});
if (typeof overdue.pendingCount !== 'number') {
throw new Error('overdue-summary missing pendingCount');
}
console.log('10. Refund ticket flow');
const order2 = await req('USER_H5', '/trade/orders', {
method: 'POST',
token: userToken,
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
});
await req('USER_H5', `/trade/orders/${order2.id}/pay`, { method: 'POST', token: userToken });
await req('USER_H5', `/trade/orders/${order2.id}/refund-requests`, {
method: 'POST',
token: userToken,
body: JSON.stringify({ remark: 'smoke refund' }),
});
const tickets = await req('HQ_WEB', '/admin/tickets?ticketType=REFUND', {
token: admin.accessToken,
});
const refundTicket = tickets.items?.find((t) => t.refId === order2.id);
if (!refundTicket) throw new Error('Refund ticket not created');
await req('HQ_WEB', `/admin/tickets/${refundTicket.id}/approve`, {
method: 'POST',
token: admin.accessToken,
body: JSON.stringify({}),
});
const refundedOrder = await req('USER_H5', `/trade/orders/${order2.id}`, { token: userToken });
if (refundedOrder.status !== 'REFUNDED' || refundedOrder.payStatus !== 'REFUNDED') {
throw new Error(
`Refund order status expected REFUNDED/REFUNDED, got ${refundedOrder.status}/${refundedOrder.payStatus}`,
);
}
console.log('11. Partner phone gate');
const unknownPartner = await expectFail('PARTNER_H5', '/partner/auth/phone/check', {
method: 'POST',
body: JSON.stringify({ phone: '13899999999' }),
});
if (!unknownPartner.includes('未找到合伙人账号')) {
throw new Error(`Expected partner phone gate, got: ${unknownPartner}`);
}
console.log('12. Partner bill');
const partners = await req('HQ_WEB', '/admin/partners', { token: admin.accessToken });
const partnerId = partners.items?.[0]?.id;
if (partnerId) {
const now = new Date();
await req('HQ_WEB', '/admin/partner-bills/generate', {
method: 'POST',
token: admin.accessToken,
body: JSON.stringify({ partnerId, year: now.getFullYear(), month: now.getMonth() + 1 }),
});
}
console.log('\n✅ V3 smoke passed');
}
main().catch((e) => {
console.error('❌ V3 smoke failed:', e.message);
process.exit(1);
});