Files
zhuomi-hui/server/test/api.test.js
T

172 lines
7.3 KiB
JavaScript
Raw Normal View History

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,
body: {
activityId: '93',
name: '张三',
phone: '13800000000',
company: '=cmd',
sourceSharer: user.openid,
remindStatus: 'accept'
}
})
assert.strictEqual(signup.status, 200)
assert.strictEqual(signup.body.data.remindAccepted, true)
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)
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)
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)
})