#!/usr/bin/env node /** * 初始化清单「验收」的真实链路脚本:注册 → 申请 → 运营审核 → 积分记账与幂等。 * * 前提:voko-server 已启动(VOKO_API,默认 http://localhost:9201),.env 已配置真实 SMTP; * VOKO_TEST_EMAIL:一个能收信的邮箱,脚本用 plus 地址注册新用户(如 you+voko-xxx@gmail.com); * VOKO_ADMIN_EMAIL / VOKO_ADMIN_PASSWORD:一个已用 admin:grant 设为管理员的账号。 * 缺任一项输出 BLOCKED 并以 2 退出,不用替身补绿。 * 结束时只清理本脚本创建的数据。 */ import { randomUUID } from 'node:crypto' import { createBillingRepo, createLedger } from '@drama/billing' import { createDb } from '@drama/db' import { databaseUrlFromArgs } from '../../../src/db/env-file.js' const api = (process.env.VOKO_API || 'http://localhost:9201').replace(/\/$/, '') const missing = ['VOKO_TEST_EMAIL', 'VOKO_ADMIN_EMAIL', 'VOKO_ADMIN_PASSWORD'].filter((name) => !process.env[name]) if (missing.length > 0) { console.log(JSON.stringify({ status: 'BLOCKED', missing })) process.exit(2) } const db = createDb({ url: databaseUrlFromArgs(process.argv.slice(2)), pool: { min: 1, max: 2 } }) const [local, domain] = String(process.env.VOKO_TEST_EMAIL).split('@') const email = `${local}+voko-skeleton-${Date.now()}@${domain}`.toLowerCase() const results = [] let userId = null /** * @param {string} name * @param {boolean} pass * @param {unknown} [detail] */ function check(name, pass, detail) { results.push({ name, pass, ...(pass ? {} : { detail }) }) if (!pass) throw new Error(`FAILED: ${name} ${JSON.stringify(detail)}`) } /** * @param {string} method * @param {string} path * @param {{ body?: unknown, token?: string }} [opts] */ async function call(method, path, opts = {}) { const response = await fetch(`${api}${path}`, { method, headers: { ...(opts.body === undefined ? {} : { 'content-type': 'application/json' }), ...(opts.token === undefined ? {} : { authorization: `Bearer ${opts.token}` }), }, body: opts.body === undefined ? undefined : JSON.stringify(opts.body), }) return { status: response.status, json: await response.json() } } try { const health = await call('GET', '/healthz') check('healthz', health.status === 200 && health.json.ok === true, health) const sent = await call('POST', '/api/voko/auth/send-code', { body: { email, locale: 'en' } }) check('send-code', sent.status === 200, sent) const log = await db('email_delivery_logs').where({ to_email: email, template: 'verify-code', status: 'sent' }).orderBy('created_at', 'desc').first() check('验证码邮件已投递并记录', log !== undefined, { email }) const code = JSON.parse(log.metadata_json).code const registered = await call('POST', '/api/voko/auth/register', { body: { email, password: `pw-${randomUUID()}`, verification_code: code, locale: 'en' } }) check('register', registered.status === 200 && typeof registered.json.access_token === 'string', registered) userId = registered.json.user.id const token = registered.json.access_token const me0 = await call('GET', '/api/voko/me', { token }) check('新用户没有申请', me0.status === 200 && me0.json.access_request === null, me0) const submitted = await call('POST', '/api/voko/access-requests', { token, body: { note: 'skeleton scenario' } }) check('提交申请 → pending', submitted.json.access_request.status === 'pending', submitted) const again = await call('POST', '/api/voko/access-requests', { token, body: {} }) check('重复提交返回同一申请', again.json.access_request.id === submitted.json.access_request.id, again) const forbidden = await call('GET', '/api/voko/admin/access-requests', { token }) check('普通用户访问后台 → 403', forbidden.status === 403, forbidden) const admin = await call('POST', '/api/voko/auth/login', { body: { method: 'password', email: process.env.VOKO_ADMIN_EMAIL, password: process.env.VOKO_ADMIN_PASSWORD } }) check('admin login', admin.status === 200, admin) const queue = await call('GET', '/api/voko/admin/access-requests?status=pending&limit=200', { token: admin.json.access_token }) check('排队列表里有这条申请', queue.json.access_requests.some((r) => r.id === submitted.json.access_request.id), queue.json) const approved = await call('POST', `/api/voko/admin/access-requests/${submitted.json.access_request.id}/review`, { token: admin.json.access_token, body: { decision: 'approved' } }) check('审核通过', approved.status === 200 && approved.json.access_request.status === 'approved', approved) const twice = await call('POST', `/api/voko/admin/access-requests/${submitted.json.access_request.id}/review`, { token: admin.json.access_token, body: { decision: 'rejected' } }) check('重复审核 → 409', twice.status === 409, twice) const notice = await db('email_delivery_logs').where({ to_email: email, purpose: 'access_approved' }).first() check('通过通知邮件已投递', notice !== undefined && notice.status === 'sent', notice) const ledger = createLedger(createBillingRepo(db)) await ledger.grant({ user_id: userId, amount: 100, source: 'admin', reason: 'admin_adjustment', note: 'skeleton scenario', idempotency_key: `skeleton:grant:${userId}` }) await ledger.deduct({ user_id: userId, amount: 30, source: 'admin', reason: 'skeleton', idempotency_key: `skeleton:deduct:${userId}` }) const replay = await ledger.deduct({ user_id: userId, amount: 30, source: 'admin', reason: 'skeleton', idempotency_key: `skeleton:deduct:${userId}` }) check('同一幂等键重放不重复扣', replay.replayed === true && replay.balance_after === 70, replay) const view = await db('points_balance_view').where({ user_id: userId }).first() check('余额视图 = 70', Number(view.balance) === 70, view) console.log(JSON.stringify({ status: 'PASS', email, results }, null, 2)) } catch (err) { console.log(JSON.stringify({ status: 'FAIL', email, results, error: err instanceof Error ? err.message : String(err) }, null, 2)) process.exitCode = 1 } finally { if (userId !== null) { await db('billing_ledger').where({ user_id: userId }).del() await db('vk_access_requests').where({ user_id: userId }).del() await db('auth_sessions').where({ user_id: userId }).del() await db('users').where({ id: userId }).del() } await db('auth_codes').where({ email }).del() await db('email_delivery_logs').where({ to_email: email }).del() await db.destroy() }