Files
memind/scripts/verify-template-catalog-e2e.mjs
T
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 08:06:12 +08:00

212 lines
7.5 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.
#!/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_PASSWORDmemind_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);
});