ab10431001
Co-authored-by: Cursor <cursoragent@cursor.com>
286 lines
10 KiB
JavaScript
286 lines
10 KiB
JavaScript
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);
|
||
});
|