212 lines
7.5 KiB
JavaScript
212 lines
7.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* E2E: Portal template catalog balance purchase + WeChat checkout creation.
|
|||
|
|
* Usage: node scripts/verify-template-catalog-e2e.mjs
|
|||
|
|
*/
|
|||
|
|
import fs from 'node:fs';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
import { loadH5Environment } from './load-env.mjs';
|
|||
|
|
import { createReporter, loginViaApi } from './scenario-test-lib.mjs';
|
|||
|
|
|
|||
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|||
|
|
const root = path.join(scriptDir, '..');
|
|||
|
|
|
|||
|
|
function loadEnvFile(filePath) {
|
|||
|
|
if (!fs.existsSync(filePath)) return;
|
|||
|
|
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|||
|
|
const trimmed = line.trim();
|
|||
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|||
|
|
const eq = trimmed.indexOf('=');
|
|||
|
|
if (eq <= 0) continue;
|
|||
|
|
const key = trimmed.slice(0, eq).trim();
|
|||
|
|
let value = trimmed.slice(eq + 1).trim();
|
|||
|
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|||
|
|
value = value.slice(1, -1);
|
|||
|
|
}
|
|||
|
|
if (!(key in process.env)) process.env[key] = value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadH5Environment(scriptDir);
|
|||
|
|
loadEnvFile(path.join(root, '../memind_adm/.env'));
|
|||
|
|
|
|||
|
|
const portalBase = process.env.PORTAL_API_BASE ?? `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
|||
|
|
const adminBase = process.env.ADM_DEV_BACKEND ?? 'http://127.0.0.1:8085';
|
|||
|
|
const adminUser = process.env.H5_ADMIN_USERNAME?.trim() || 'admin';
|
|||
|
|
const adminPass =
|
|||
|
|
process.env.H5_ADMIN_PASSWORD?.trim() ||
|
|||
|
|
process.env.ADMIN_PASSWORD?.trim() ||
|
|||
|
|
'';
|
|||
|
|
const portalUser =
|
|||
|
|
process.env.TEMPLATE_E2E_USERNAME?.trim() ||
|
|||
|
|
process.env.MEMIND_E2E_USERNAME?.trim() ||
|
|||
|
|
'john4';
|
|||
|
|
const portalPass =
|
|||
|
|
process.env.TEMPLATE_E2E_PASSWORD?.trim() ||
|
|||
|
|
process.env.MEMIND_E2E_PASSWORD?.trim() ||
|
|||
|
|
process.env.JOHN4_PASSWORD?.trim() ||
|
|||
|
|
'888888';
|
|||
|
|
|
|||
|
|
const PURCHASE_SKILL = process.env.TEMPLATE_E2E_SKILL?.trim() || 'page-template-travel';
|
|||
|
|
|
|||
|
|
async function api(base, method, urlPath, { body, cookie } = {}) {
|
|||
|
|
const res = await fetch(`${base}${urlPath}`, {
|
|||
|
|
method,
|
|||
|
|
headers: {
|
|||
|
|
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|||
|
|
...(cookie ? { Cookie: cookie } : {}),
|
|||
|
|
},
|
|||
|
|
body: body ? JSON.stringify(body) : undefined,
|
|||
|
|
});
|
|||
|
|
const text = await res.text();
|
|||
|
|
let json = null;
|
|||
|
|
try {
|
|||
|
|
json = text ? JSON.parse(text) : null;
|
|||
|
|
} catch {
|
|||
|
|
json = { raw: text.slice(0, 400) };
|
|||
|
|
}
|
|||
|
|
return { status: res.status, json, text };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
const reporter = createReporter();
|
|||
|
|
console.log(`==> Portal ${portalBase}`);
|
|||
|
|
console.log(`==> Admin ${adminBase}`);
|
|||
|
|
console.log(`==> 购买目标 skill: ${PURCHASE_SKILL}`);
|
|||
|
|
|
|||
|
|
if (!adminPass) {
|
|||
|
|
throw new Error('缺少 H5_ADMIN_PASSWORD(memind_adm/.env)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const adminAuth = await loginViaApi(adminBase, { username: adminUser, password: adminPass }, reporter);
|
|||
|
|
|
|||
|
|
let portalAuth;
|
|||
|
|
try {
|
|||
|
|
portalAuth = await loginViaApi(
|
|||
|
|
portalBase,
|
|||
|
|
{ username: portalUser, password: portalPass },
|
|||
|
|
{
|
|||
|
|
pass: (label, detail) => reporter.pass(`Portal ${label}`, detail),
|
|||
|
|
fail: (label, detail) => reporter.fail(`Portal ${label}`, detail),
|
|||
|
|
checks: reporter.checks,
|
|||
|
|
issues: reporter.issues,
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
reporter.pass('Portal 测试账号', portalUser);
|
|||
|
|
} catch (err) {
|
|||
|
|
throw new Error(`Portal 测试账号 ${portalUser} 登录失败: ${err.message}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const rechargeRes = await api(adminBase, 'POST', `/admin-api/users/${portalAuth.user.id}/recharge`, {
|
|||
|
|
cookie: adminAuth.cookie,
|
|||
|
|
body: { amountCents: 5000, note: 'template-catalog-e2e' },
|
|||
|
|
});
|
|||
|
|
if (rechargeRes.status === 200) {
|
|||
|
|
reporter.pass('Admin 充值', `+5000 分 → ${portalUser}`);
|
|||
|
|
} else {
|
|||
|
|
reporter.fail('Admin 充值', `HTTP ${rechargeRes.status}: ${JSON.stringify(rechargeRes.json)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const catalogRes = await api(portalBase, 'GET', '/api/mindspace/v1/template-catalog', {
|
|||
|
|
cookie: portalAuth.cookie,
|
|||
|
|
});
|
|||
|
|
if (catalogRes.status !== 200) {
|
|||
|
|
throw new Error(`catalog HTTP ${catalogRes.status}: ${JSON.stringify(catalogRes.json)}`);
|
|||
|
|
}
|
|||
|
|
const items = catalogRes.json?.data?.items ?? [];
|
|||
|
|
reporter.pass('模板列表', `${items.length} 项`);
|
|||
|
|
|
|||
|
|
const target = items.find((item) => item.skillName === PURCHASE_SKILL);
|
|||
|
|
if (!target) {
|
|||
|
|
throw new Error(`缺少 ${PURCHASE_SKILL}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pickCheckoutSkill = () => {
|
|||
|
|
const notOwned = items.filter((item) => !item.owned && item.priceCents > 0);
|
|||
|
|
return notOwned[0]?.skillName ?? PURCHASE_SKILL;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (target.owned) {
|
|||
|
|
reporter.pass('余额购买', `跳过:${PURCHASE_SKILL} 已拥有`);
|
|||
|
|
} else {
|
|||
|
|
const meBefore = await api(portalBase, 'GET', '/auth/me', { cookie: portalAuth.cookie });
|
|||
|
|
const balanceBefore = Number(meBefore.json?.user?.balance_cents ?? 0);
|
|||
|
|
reporter.pass('购买前余额', `${balanceBefore} 分`);
|
|||
|
|
|
|||
|
|
const purchaseRes = await api(
|
|||
|
|
portalBase,
|
|||
|
|
'POST',
|
|||
|
|
`/api/mindspace/v1/template-catalog/${encodeURIComponent(PURCHASE_SKILL)}/purchase`,
|
|||
|
|
{ cookie: portalAuth.cookie },
|
|||
|
|
);
|
|||
|
|
if (purchaseRes.status === 402) {
|
|||
|
|
reporter.fail('余额购买', `402 余额不足: ${JSON.stringify(purchaseRes.json)}`);
|
|||
|
|
} else if (purchaseRes.status !== 200) {
|
|||
|
|
reporter.fail('余额购买', `HTTP ${purchaseRes.status}: ${JSON.stringify(purchaseRes.json)}`);
|
|||
|
|
} else {
|
|||
|
|
const result = purchaseRes.json?.data ?? purchaseRes.json;
|
|||
|
|
reporter.pass(
|
|||
|
|
'余额购买',
|
|||
|
|
`price=${result.priceCents ?? target.priceCents} balance=${result.balanceCents ?? '?'}`,
|
|||
|
|
);
|
|||
|
|
const skills = result.grantedSkills ?? [];
|
|||
|
|
if (skills.includes(PURCHASE_SKILL)) {
|
|||
|
|
reporter.pass('skill 开通', PURCHASE_SKILL);
|
|||
|
|
} else {
|
|||
|
|
reporter.fail('skill 开通', `grantedSkills 未含 ${PURCHASE_SKILL}: ${JSON.stringify(skills)}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const wechatSkill = pickCheckoutSkill();
|
|||
|
|
|
|||
|
|
const checkoutRes = await api(
|
|||
|
|
portalBase,
|
|||
|
|
'POST',
|
|||
|
|
`/api/mindspace/v1/template-catalog/${encodeURIComponent(wechatSkill)}/checkout`,
|
|||
|
|
{
|
|||
|
|
cookie: portalAuth.cookie,
|
|||
|
|
body: { payScene: 'native' },
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
const checkout = checkoutRes.json?.data ?? checkoutRes.json;
|
|||
|
|
if (checkoutRes.status === 200 && checkout?.ok && checkout?.order) {
|
|||
|
|
reporter.pass(
|
|||
|
|
'微信 checkout',
|
|||
|
|
`${wechatSkill} pending=${checkout.order.status} mode=${checkout.order.payMode}`,
|
|||
|
|
);
|
|||
|
|
if (checkout.order.codeUrl || checkout.order.jsapiParams || checkout.order.h5Url) {
|
|||
|
|
reporter.pass('微信支付参数', checkout.order.codeUrl ? 'native codeUrl' : checkout.order.payMode);
|
|||
|
|
}
|
|||
|
|
} else if (checkoutRes.status === 400 && String(checkout?.error?.message ?? checkout?.message ?? '').includes('已拥有')) {
|
|||
|
|
reporter.pass('微信 checkout', `${wechatSkill} 已拥有,跳过`);
|
|||
|
|
} else if (
|
|||
|
|
checkoutRes.status === 400 &&
|
|||
|
|
String(checkout?.error?.message ?? checkout?.message ?? '').includes('微信支付尚未配置')
|
|||
|
|
) {
|
|||
|
|
reporter.pass('微信 checkout', '微信支付未配置(本地预期可接受)');
|
|||
|
|
} else {
|
|||
|
|
reporter.fail('微信 checkout', `HTTP ${checkoutRes.status}: ${JSON.stringify(checkoutRes.json)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const previewRes = await api(
|
|||
|
|
portalBase,
|
|||
|
|
'GET',
|
|||
|
|
`/api/mindspace/v1/template-catalog/page-template-travel/preview`,
|
|||
|
|
{ cookie: portalAuth.cookie },
|
|||
|
|
);
|
|||
|
|
if (previewRes.status === 200 && previewRes.text.includes('周末去哪玩')) {
|
|||
|
|
reporter.pass('预览 HTML', 'travel 示例文案正确');
|
|||
|
|
} else {
|
|||
|
|
reporter.fail('预览 HTML', `HTTP ${previewRes.status}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process.exitCode = reporter.summary();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch((err) => {
|
|||
|
|
console.error('\nE2E 失败:', err.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|