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