完善了安全功能
This commit is contained in:
265
test/api.test.js
Normal file
265
test/api.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user