2026-09-29 19:01:36 +08:00
|
|
|
const test = require('node:test')
|
|
|
|
|
const assert = require('node:assert')
|
|
|
|
|
const fs = require('fs')
|
|
|
|
|
const os = require('os')
|
|
|
|
|
const path = require('path')
|
|
|
|
|
|
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zhuomi-'))
|
|
|
|
|
process.env.DB_FILE = path.join(tmp, 'db.json')
|
|
|
|
|
process.env.UPLOAD_DIR = path.join(tmp, 'uploads')
|
|
|
|
|
delete process.env.WX_APPID
|
|
|
|
|
delete process.env.WX_SECRET
|
|
|
|
|
|
|
|
|
|
const app = require('../app')
|
|
|
|
|
|
|
|
|
|
let base = ''
|
|
|
|
|
let server
|
|
|
|
|
|
|
|
|
|
test.before(async () => {
|
|
|
|
|
server = app.listen(0)
|
|
|
|
|
await new Promise((resolve) => server.once('listening', resolve))
|
|
|
|
|
base = `http://127.0.0.1:${server.address().port}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test.after(() => {
|
|
|
|
|
server.close()
|
|
|
|
|
fs.rmSync(tmp, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function call(method, url, options) {
|
|
|
|
|
const opts = options || {}
|
|
|
|
|
const headers = {}
|
|
|
|
|
if (opts.token) headers.Authorization = opts.token
|
|
|
|
|
let body
|
|
|
|
|
if (opts.form) body = opts.form
|
|
|
|
|
else if (opts.body) {
|
|
|
|
|
headers['Content-Type'] = 'application/json'
|
|
|
|
|
body = JSON.stringify(opts.body)
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(base + url, { method, headers, body })
|
|
|
|
|
const type = res.headers.get('content-type') || ''
|
|
|
|
|
return { status: res.status, body: type.includes('json') ? await res.json() : await res.text() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function login(deviceId) {
|
|
|
|
|
const res = await call('POST', '/api/login', { body: { code: 'x', deviceId } })
|
|
|
|
|
assert.strictEqual(res.status, 200)
|
|
|
|
|
return res.body.data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('seeded admin password is stored hashed', async () => {
|
|
|
|
|
await call('GET', '/api/activity/current')
|
|
|
|
|
const data = JSON.parse(fs.readFileSync(process.env.DB_FILE, 'utf8'))
|
|
|
|
|
assert.match(data.admins[0].password, /^scrypt\$/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('full flow: admin, past activity, signup, checkin, export', async () => {
|
|
|
|
|
const adminLogin = await call('POST', '/api/admin/login', { body: { username: 'admin', password: '123456' } })
|
|
|
|
|
const adminToken = adminLogin.body.data.token
|
|
|
|
|
assert.ok(adminToken)
|
|
|
|
|
assert.strictEqual((await call('POST', '/api/admin/login', { body: { username: 'admin', password: 'bad' } })).status, 401)
|
|
|
|
|
|
|
|
|
|
const user = await login('device-a')
|
|
|
|
|
assert.strictEqual(user.isAdmin, false)
|
|
|
|
|
|
|
|
|
|
const denied = await call('POST', '/api/activity/past', { token: user.token, body: { title: 'x', startAt: 'y' } })
|
|
|
|
|
assert.strictEqual(denied.status, 403)
|
|
|
|
|
|
|
|
|
|
await call('POST', '/api/admin/allowlist', { token: adminToken, body: { openid: user.openid } })
|
|
|
|
|
const profile = await call('GET', '/api/user/profile', { token: user.token })
|
|
|
|
|
assert.strictEqual(profile.body.data.isAdmin, true)
|
|
|
|
|
|
|
|
|
|
const first = await call('POST', '/api/activity/past', { token: user.token, body: { title: '往期一', startAt: '2026-01-01' } })
|
|
|
|
|
const second = await call('POST', '/api/activity/past', { token: user.token, body: { title: '往期二', startAt: '2026-02-01' } })
|
|
|
|
|
assert.strictEqual(first.body.data.theme, 0)
|
|
|
|
|
assert.strictEqual(second.body.data.theme, 1)
|
|
|
|
|
|
|
|
|
|
await call('POST', `/api/activity/past/${first.body.data.id}/pin`, { token: user.token, body: { pinned: true } })
|
|
|
|
|
const past = await call('GET', '/api/activity/past')
|
|
|
|
|
assert.deepStrictEqual(past.body.data.map((item) => item.title), ['往期一', '往期二'])
|
|
|
|
|
|
|
|
|
|
const del = await call('DELETE', `/api/activity/past/${second.body.data.id}`, { token: user.token })
|
|
|
|
|
assert.strictEqual(del.status, 200)
|
|
|
|
|
|
|
|
|
|
const bad = await call('POST', '/api/signup', { token: user.token, body: { activityId: '93', name: '张三', phone: '123' } })
|
|
|
|
|
assert.strictEqual(bad.status, 400)
|
|
|
|
|
|
|
|
|
|
const guest = await login('device-b')
|
|
|
|
|
const signup = await call('POST', '/api/signup', {
|
|
|
|
|
token: guest.token,
|
2026-10-08 13:06:43 +08:00
|
|
|
body: {
|
|
|
|
|
activityId: '93',
|
|
|
|
|
name: '张三',
|
|
|
|
|
phone: '13800000000',
|
|
|
|
|
company: '=cmd',
|
|
|
|
|
sourceSharer: user.openid,
|
|
|
|
|
remindStatus: 'accept'
|
|
|
|
|
}
|
2026-09-29 19:01:36 +08:00
|
|
|
})
|
|
|
|
|
assert.strictEqual(signup.status, 200)
|
2026-10-08 13:06:43 +08:00
|
|
|
assert.strictEqual(signup.body.data.remindAccepted, true)
|
2026-09-29 19:01:36 +08:00
|
|
|
const dup = await call('POST', '/api/signup', { token: guest.token, body: { activityId: '93', name: '张三', phone: '13800000000' } })
|
|
|
|
|
assert.strictEqual(dup.status, 400)
|
|
|
|
|
|
|
|
|
|
const checkin = await call('POST', '/api/checkin/scan', { token: guest.token, body: { simulate: true } })
|
|
|
|
|
assert.strictEqual(checkin.body.data.checkedIn, true)
|
|
|
|
|
const mine = await call('GET', '/api/signup/mine', { token: guest.token })
|
|
|
|
|
assert.strictEqual(mine.body.data.checkins.length, 1)
|
|
|
|
|
|
|
|
|
|
const stats = await call('GET', '/api/admin/stats', { token: adminToken })
|
|
|
|
|
assert.strictEqual(stats.body.data.checkinCount, 1)
|
|
|
|
|
|
2026-10-08 13:06:43 +08:00
|
|
|
await call('PUT', '/api/admin/settings', { token: adminToken, body: { subscribeTmplId: 'tmpl_test_id' } })
|
|
|
|
|
const config = await call('GET', '/api/subscribe/config')
|
|
|
|
|
assert.deepStrictEqual(config.body.data.tmplIds, ['tmpl_test_id'])
|
|
|
|
|
const remind = await call('POST', '/api/admin/activities/93/remind', { token: adminToken, body: {} })
|
|
|
|
|
assert.strictEqual(remind.status, 200)
|
|
|
|
|
assert.strictEqual(remind.body.data.sent, 1)
|
|
|
|
|
|
2026-09-29 19:01:36 +08:00
|
|
|
const csv = await call('GET', `/api/admin/signups/export?token=${adminToken}`)
|
|
|
|
|
assert.strictEqual(csv.status, 200)
|
|
|
|
|
assert.match(csv.body, /张三,13800000000,'=cmd/)
|
|
|
|
|
|
|
|
|
|
const queryOnly = await call('GET', `/api/admin/stats?token=${adminToken}`)
|
|
|
|
|
assert.strictEqual(queryOnly.status, 401)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('profile rejects temp avatar paths and accepts uploaded avatars', async () => {
|
|
|
|
|
const user = await login('device-c')
|
|
|
|
|
const temp = await call('POST', '/api/user/profile', {
|
|
|
|
|
token: user.token,
|
|
|
|
|
body: { nickName: '小幂', avatarUrl: 'http://tmp/abc.png' }
|
|
|
|
|
})
|
|
|
|
|
assert.strictEqual(temp.body.data.nickName, '小幂')
|
|
|
|
|
assert.strictEqual(temp.body.data.avatarUrl, '')
|
|
|
|
|
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('file', new Blob([Buffer.from('89504e47', 'hex')], { type: 'image/png' }), 'a.png')
|
|
|
|
|
const upload = await call('POST', '/api/user/avatar', { token: user.token, form })
|
|
|
|
|
assert.strictEqual(upload.status, 200)
|
|
|
|
|
assert.match(upload.body.data.url, /^\/static\/uploads\/.+\.png$/)
|
|
|
|
|
|
|
|
|
|
const saved = await call('POST', '/api/user/profile', { token: user.token, body: { avatarUrl: upload.body.data.url } })
|
|
|
|
|
assert.strictEqual(saved.body.data.avatarUrl, upload.body.data.url)
|
|
|
|
|
|
|
|
|
|
const served = await fetch(base + upload.body.data.url)
|
|
|
|
|
assert.strictEqual(served.status, 200)
|
|
|
|
|
|
|
|
|
|
const video = new FormData()
|
|
|
|
|
video.append('file', new Blob([Buffer.from('00')], { type: 'video/mp4' }), 'a.mp4')
|
|
|
|
|
const rejected = await call('POST', '/api/user/avatar', { token: user.token, form: video })
|
|
|
|
|
assert.strictEqual(rejected.status, 400)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('admin activity CRUD keeps a single current activity', async () => {
|
|
|
|
|
const token = (await call('POST', '/api/admin/login', { body: { username: 'admin', password: '123456' } })).body.data.token
|
|
|
|
|
const created = await call('POST', '/api/admin/activities', {
|
|
|
|
|
token,
|
|
|
|
|
body: { title: '第94期', status: 'current', isCurrent: true, startAt: '2026-07-01' }
|
|
|
|
|
})
|
|
|
|
|
assert.strictEqual(created.status, 200)
|
|
|
|
|
const list = (await call('GET', '/api/admin/activities', { token })).body.data
|
|
|
|
|
assert.deepStrictEqual(list.filter((item) => item.isCurrent).map((item) => item.id), [created.body.data.id])
|
|
|
|
|
|
|
|
|
|
const current = await call('GET', '/api/activity/current')
|
|
|
|
|
assert.strictEqual(current.body.data.title, '第94期')
|
|
|
|
|
|
|
|
|
|
const draft = await call('POST', '/api/admin/activities', { token, body: { title: '草稿', status: 'draft' } })
|
|
|
|
|
assert.strictEqual((await call('GET', `/api/activity/${draft.body.data.id}`)).status, 404)
|
|
|
|
|
|
|
|
|
|
assert.strictEqual((await call('GET', '/api/nope')).status, 404)
|
|
|
|
|
})
|