import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import supertest from 'supertest'; import { APP } from '../app.js'; import { DBModel } from '../models/index.js'; describe('用户接口集成测试', () => { let app, agent; const testMobile = '13900000001'; const testPasswd = 'test123456'; let token = ''; let userId = ''; before(async () => { app = new APP(); agent = supertest(app.app.callback()); // 等待数据库连接 await new Promise(resolve => setTimeout(resolve, 2000)); }); after(async () => { // 清理测试数据 if (userId) { await DBModel.User.delUser(userId); } app.stop(); }); describe('POST /user/register', () => { it('应成功注册新用户', async () => { const res = await agent .post('/user/register') .send({ userInfo: { profile: { mobile: testMobile, name: '测试用户' }, security: { passwd: testPasswd }, }, }); assert.equal(res.body.code, 0, `注册失败: ${res.body.msg}`); assert.ok(res.body.data.user); assert.ok(res.body.data.user.security.token); assert.equal(res.body.data.user.profile.mobile, testMobile); // 密码字段不应返回 assert.equal(res.body.data.user.security.passwd, undefined); assert.equal(res.body.data.user.security.passwdSalt, undefined); token = res.body.data.user.security.token; userId = res.body.data.user._id; }); it('重复注册应返回 409', async () => { const res = await agent .post('/user/register') .send({ userInfo: { profile: { mobile: testMobile, name: '测试用户' }, security: { passwd: testPasswd }, }, }); assert.equal(res.body.code, 409); assert.equal(res.body.msg, '手机号已注册'); }); it('缺少手机号应返回 400', async () => { const res = await agent .post('/user/register') .send({ userInfo: { profile: { name: '测试用户' }, security: { passwd: testPasswd }, }, }); assert.equal(res.body.code, 400); }); it('缺少密码应返回 400', async () => { const res = await agent .post('/user/register') .send({ userInfo: { profile: { mobile: '13900000099' }, security: {}, }, }); assert.equal(res.body.code, 400); }); it('密码少于6位应返回 400', async () => { const res = await agent .post('/user/register') .send({ userInfo: { profile: { mobile: '13900000098' }, security: { passwd: '123' }, }, }); assert.equal(res.body.code, 400); }); }); describe('POST /user/signin', () => { it('用正确密码登录应成功', async () => { const res = await agent .post('/user/signin') .send({ mobile: testMobile, passwd: testPasswd }); assert.equal(res.body.code, 0, `登录失败: ${res.body.msg}`); assert.ok(res.body.data.user.security.token); // 更新 token token = res.body.data.user.security.token; // 应记录最后登录信息 assert.ok(res.body.data.user.security.lastLoginAt); }); it('错误密码应返回 401', async () => { const res = await agent .post('/user/signin') .send({ mobile: testMobile, passwd: 'wrongpassword' }); assert.equal(res.body.code, 401); }); it('不存在的用户应返回 401', async () => { const res = await agent .post('/user/signin') .send({ mobile: '13999999999', passwd: 'test123456' }); assert.equal(res.body.code, 401); }); it('缺少手机号应返回 400', async () => { const res = await agent .post('/user/signin') .send({ passwd: testPasswd }); assert.equal(res.body.code, 400); }); }); describe('POST /user/userInfo', () => { it('用 Authorization Bearer 获取用户信息', async () => { const res = await agent .post('/user/userInfo') .set('Authorization', `Bearer ${token}`) .send({}); assert.equal(res.body.code, 0, `获取用户信息失败: ${res.body.msg}`); assert.equal(res.body.data.user.profile.mobile, testMobile); assert.equal(res.body.data.user.security.passwd, undefined); }); it('用 body token 获取用户信息(向后兼容)', async () => { const res = await agent .post('/user/userInfo') .send({ token }); assert.equal(res.body.code, 0); assert.equal(res.body.data.user.profile.mobile, testMobile); }); it('无效 token 应返回 401', async () => { const res = await agent .post('/user/userInfo') .set('Authorization', 'Bearer invalid_token') .send({}); assert.equal(res.body.code, 401); }); it('缺少 token 应返回 400', async () => { const res = await agent .post('/user/userInfo') .send({}); assert.equal(res.body.code, 400); }); }); describe('POST /user/update', () => { it('更新用户信息应成功', async () => { const res = await agent .post('/user/update') .set('Authorization', `Bearer ${token}`) .send({ profile: { name: '更新后的名字' }, }); assert.equal(res.body.code, 0, `更新失败: ${res.body.msg}`); assert.equal(res.body.data.user.profile.name, '更新后的名字'); }); it('无 token 更新应返回 400', async () => { const res = await agent .post('/user/update') .send({ profile: { name: 'test' } }); assert.equal(res.body.code, 400); }); }); describe('POST /user/signout', () => { it('退出登录应成功', async () => { const res = await agent .post('/user/signout') .set('Authorization', `Bearer ${token}`) .send({}); assert.equal(res.body.code, 0); assert.equal(res.body.msg, '退出登录成功'); }); it('退出后用旧 token 获取信息应返回 401', async () => { const res = await agent .post('/user/userInfo') .set('Authorization', `Bearer ${token}`) .send({}); assert.equal(res.body.code, 401); }); }); describe('密码迁移:MD5 → bcrypt', () => { it('旧 MD5 用户登录应自动升级密码', async () => { // 先注册一个新用户 const mobile = '13900000002'; const passwd = 'md5test123'; // 直接写入一个 MD5 密码的用户 const crypto = await import('crypto'); const salt = crypto.randomBytes(8).toString('hex'); const md5Hash = crypto.createHash('md5').update(passwd + salt).digest('hex'); const newUser = { profile: { mobile, name: 'MD5测试用户' }, security: { passwd: md5Hash, passwdSalt: salt }, status: { account: 'normal' }, app: {}, social: { wechat: {} }, }; const user = await DBModel.User.setUser(newUser); assert.ok(user); // 用旧密码登录 const res = await agent .post('/user/signin') .send({ mobile, passwd }); assert.equal(res.body.code, 0, `MD5迁移登录失败: ${res.body.msg}`); // 验证密码已升级为 bcrypt const updatedUser = await DBModel.User.findOne({ 'profile.mobile': mobile }); assert.ok(updatedUser.security.passwd.startsWith('$2'), '密码应已升级为 bcrypt'); assert.equal(updatedUser.security.passwdSalt, undefined, 'salt 应已清除'); // 清理 await DBModel.User.delUser(updatedUser._id); }); }); });