skeleton.mjs
6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/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)
// auth.sendCode 不等待 SMTP 就返回,这里等真实投递落库(最多 30 秒)。
let log
for (let i = 0; i < 30 && log === undefined; i += 1) {
log = await db('email_delivery_logs').where({ to_email: email, template: 'verify-code', status: 'sent' }).orderBy('created_at', 'desc').first()
if (log === undefined) await new Promise((resolve) => setTimeout(resolve, 1000))
}
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()
}