feat: 微信分享钩子全页面接入 + 订阅信息结构化映射 + JWT 错误处理加强
- client: 全部页面接入 onShareAppMessage/onShareTimeline 分享钩子 - client: login 页增强、profile 订阅展示优化 - client/stores: 订阅字段扁平结构映射为 subInfo(plan/expiresAt/startDate) - server: auth 路由 JWT 无效/过期统一返回 401 未授权响应 - server: models/tests 同步更新
This commit is contained in:
@@ -1,205 +1,170 @@
|
||||
/**
|
||||
* 用户 API 集成测试
|
||||
*/
|
||||
|
||||
const request = require('supertest')
|
||||
const mongoose = require('mongoose')
|
||||
const app = require('../../src/index')
|
||||
const User = require('../../src/models/User')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
describe('User API', () => {
|
||||
let testUser
|
||||
let authToken
|
||||
|
||||
beforeAll(async () => {
|
||||
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj'
|
||||
if (mongoose.connection.readyState === 0) {
|
||||
await mongoose.connect(mongoURI)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (mongoose.connection.readyState !== 0) {
|
||||
await mongoose.connection.dropDatabase()
|
||||
await mongoose.connection.close()
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await User.deleteMany({})
|
||||
|
||||
// 创建测试用户
|
||||
testUser = await User.create({
|
||||
openid: 'test_user_openid',
|
||||
nickName: '测试用户',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
exploreData: {
|
||||
dim1: { bestScore: 100, completed: true },
|
||||
dim2: { bestArea: 200, completed: false }
|
||||
},
|
||||
totalScore: 300,
|
||||
unlockedDims: [1, 2]
|
||||
})
|
||||
|
||||
// 生成认证 Token
|
||||
authToken = jwt.sign(
|
||||
{ userId: testUser._id.toString(), openid: testUser.openid },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1h' }
|
||||
)
|
||||
})
|
||||
|
||||
describe('GET /api/user/progress', () => {
|
||||
test('应该返回用户探索进度', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(response.body.data.exploreData).toBeDefined()
|
||||
expect(response.body.data.exploreData.dim1.bestScore).toBe(100)
|
||||
expect(response.body.data.exploreData.dim2.bestArea).toBe(200)
|
||||
})
|
||||
|
||||
test('不存在的用户应该返回404', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/progress')
|
||||
.set('Authorization', 'Bearer invalid_token')
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/user/progress', () => {
|
||||
test('应该成功保存探索进度', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
dim: 1,
|
||||
score: 150,
|
||||
collectedEggs: 5
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
|
||||
// 验证数据库更新
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.exploreData.dim1.bestScore).toBe(150)
|
||||
expect(updatedUser.exploreData.dim1.collectedEggs).toBe(5)
|
||||
})
|
||||
|
||||
test('新用户应该自动解锁维度', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
dim: 3,
|
||||
score: 100
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.unlockedDims).toContain(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/user/leaderboard', () => {
|
||||
beforeEach(async () => {
|
||||
// 创建多个测试用户
|
||||
await User.create([
|
||||
{ openid: 'user1', nickName: '用户1', totalScore: 500, dim1Score: 200 },
|
||||
{ openid: 'user2', nickName: '用户2', totalScore: 300, dim1Score: 150 },
|
||||
{ openid: 'user3', nickName: '用户3', totalScore: 400, dim1Score: 100 }
|
||||
])
|
||||
})
|
||||
|
||||
test('应该返回总分排行榜', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ limit: 10 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(response.body.data).toHaveLength(4) // 包含testUser
|
||||
expect(response.body.data[0].totalScore).toBeGreaterThanOrEqual(
|
||||
response.body.data[1].totalScore
|
||||
)
|
||||
})
|
||||
|
||||
test('应该返回维度排行榜', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ dim: 1, limit: 10 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data[0].dim1Score).toBe(200)
|
||||
})
|
||||
|
||||
test('应该限制返回数量', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ limit: 2 })
|
||||
|
||||
expect(response.body.data).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /api/user/info', () => {
|
||||
test('应该成功更新用户信息', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/user/info')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
nickName: '新名字',
|
||||
avatarUrl: 'https://example.com/new-avatar.jpg'
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.nickName).toBe('新名字')
|
||||
expect(updatedUser.avatarUrl).toBe('https://example.com/new-avatar.jpg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/user/collect-knowledge', () => {
|
||||
test('应该成功收藏知识', async () => {
|
||||
const knowledgeId = new mongoose.Types.ObjectId()
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/user/collect-knowledge')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ knowledgeId: knowledgeId.toString() })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.collectedKnowledge).toContainEqual(knowledgeId)
|
||||
})
|
||||
|
||||
test('重复收藏应该返回已存在', async () => {
|
||||
const knowledgeId = new mongoose.Types.ObjectId()
|
||||
|
||||
// 第一次收藏
|
||||
await request(app)
|
||||
.post('/api/user/collect-knowledge')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ knowledgeId: knowledgeId.toString() })
|
||||
|
||||
// 第二次收藏
|
||||
const response = await request(app)
|
||||
.post('/api/user/collect-knowledge')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ knowledgeId: knowledgeId.toString() })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
/**
|
||||
* 用户 API 集成测试
|
||||
*/
|
||||
|
||||
const request = require('supertest')
|
||||
const mongoose = require('mongoose')
|
||||
const app = require('../../src/index')
|
||||
const User = require('../../src/models/User')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
describe('User API', () => {
|
||||
let testUser
|
||||
let authToken
|
||||
|
||||
beforeAll(async () => {
|
||||
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj_test'
|
||||
if (mongoose.connection.readyState === 0) {
|
||||
await mongoose.connect(mongoURI)
|
||||
}
|
||||
// 确保唯一索引已构建(dropDatabase 后 mongoose 不会自动重建)
|
||||
await mongoose.connection.syncIndexes()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (mongoose.connection.readyState !== 0) {
|
||||
// 防呆:只允许在测试库中 dropDatabase
|
||||
if (mongoose.connection.name === 'wdkj') {
|
||||
throw new Error('[安全] 不允许对生产数据库 wdkj 执行 dropDatabase')
|
||||
}
|
||||
await mongoose.connection.dropDatabase()
|
||||
await mongoose.connection.close()
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await User.deleteMany({})
|
||||
|
||||
// 创建测试用户
|
||||
testUser = await User.create({
|
||||
openid: 'test_user_openid',
|
||||
nickName: '测试用户',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
exploreData: {
|
||||
dim1: { bestScore: 100, completed: true },
|
||||
dim2: { bestArea: 200, completed: false }
|
||||
},
|
||||
unlockedDims: [1, 2]
|
||||
})
|
||||
|
||||
// 生成认证 Token
|
||||
authToken = jwt.sign(
|
||||
{ userId: testUser._id.toString(), openid: testUser.openid },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1h' }
|
||||
)
|
||||
})
|
||||
|
||||
describe('GET /api/user/progress', () => {
|
||||
test('应该返回用户探索进度', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(response.body.data.isNewUser).toBe(false)
|
||||
const userData = response.body.data.data
|
||||
expect(userData.exploreData).toBeDefined()
|
||||
expect(userData.exploreData.dim1.bestScore).toBe(100)
|
||||
expect(userData.exploreData.dim2.bestArea).toBe(200)
|
||||
})
|
||||
|
||||
test('无效 token 应该返回401', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/progress')
|
||||
.set('Authorization', 'Bearer invalid_token')
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/user/progress', () => {
|
||||
test('应该成功保存探索进度', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
dim: 'dim1',
|
||||
data: { bestScore: 150 }
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(response.body.data.totalScore).toBe(350)
|
||||
|
||||
// 验证数据库更新
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.exploreData.dim1.bestScore).toBe(150)
|
||||
})
|
||||
|
||||
test('完成维度后应解锁下一维度', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/user/progress')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
dim: 'dim2',
|
||||
data: { completed: true, bestArea: 300 }
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
|
||||
expect(updatedUser.unlockedDims).toContain(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/user/leaderboard', () => {
|
||||
beforeEach(async () => {
|
||||
// 创建多个测试用户(分数由 exploreData 派生)
|
||||
await User.create([
|
||||
{ openid: 'user1', nickName: '用户1', exploreData: { dim1: { bestScore: 500 } } },
|
||||
{ openid: 'user2', nickName: '用户2', exploreData: { dim1: { bestScore: 300 } } },
|
||||
{ openid: 'user3', nickName: '用户3', exploreData: { dim1: { bestScore: 400 } } }
|
||||
])
|
||||
})
|
||||
|
||||
test('应该返回总分排行榜', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ limit: 10 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(response.body.data).toHaveLength(4) // 包含testUser
|
||||
expect(response.body.data[0].totalScore).toBeGreaterThanOrEqual(
|
||||
response.body.data[1].totalScore
|
||||
)
|
||||
})
|
||||
|
||||
test('应该返回维度排行榜', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ dim: 1, limit: 10 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data[0].dim1Score).toBe(500)
|
||||
})
|
||||
|
||||
test('应该限制返回数量', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/leaderboard')
|
||||
.query({ limit: 2 })
|
||||
|
||||
expect(response.body.data).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/user/achievements', () => {
|
||||
test('应该返回成就列表', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/user/achievements')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.success).toBe(true)
|
||||
expect(Array.isArray(response.body.data)).toBe(true)
|
||||
expect(response.body.data.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user