Files
Michelle ZanandCursor 0ca2e87bf2 Ship signup sheet, subscribe reminders, cover crop, and theme updates.
Wire real WeChat app config, prefer activity cover over posters, and add admin cover cropping for the home banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 13:06:43 +08:00

172 lines
7.3 KiB
JavaScript

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)
})