完善了安全功能

This commit is contained in:
lik
2026-06-12 15:24:20 +08:00
parent fba44ca015
commit ddcf200de2
12 changed files with 904 additions and 207 deletions

265
test/api.test.js Normal file
View File

@@ -0,0 +1,265 @@
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);
});
});
});