#!/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); });