feat: 添加JWT令牌刷新功能

- 新增 @nestjs/jwt 和 jsonwebtoken 依赖包
- 实现 refreshAccessToken 方法支持令牌续期
- 添加 RefreshTokenDto 和 RefreshTokenResponseDto
- 新增 /auth/refresh-token 接口
- 完善令牌刷新的限流和超时控制
- 增加相关单元测试覆盖
- 优化错误处理和日志记录
This commit is contained in:
moyin
2026-01-06 16:48:24 +08:00
parent c2ecb3c1a7
commit 3733717d1f
10 changed files with 1304 additions and 74 deletions

View File

@@ -18,6 +18,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as fc from 'fast-check';
import { LoginService } from './login.service';
import { LoginCoreService, RegisterRequest } from '../../../core/login_core/login_core.service';
@@ -97,6 +99,41 @@ describe('LoginService - Zulip账号创建属性测试', () => {
provide: ApiKeySecurityService,
useValue: mockApiKeySecurityService,
},
{
provide: JwtService,
useValue: {
sign: jest.fn().mockReturnValue('mock_jwt_token'),
signAsync: jest.fn().mockResolvedValue('mock_jwt_token'),
verify: jest.fn(),
decode: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
switch (key) {
case 'JWT_SECRET':
return 'test_jwt_secret_key_for_testing';
case 'JWT_EXPIRES_IN':
return '7d';
default:
return undefined;
}
}),
},
},
{
provide: 'UsersService',
useValue: {
findById: jest.fn(),
findByUsername: jest.fn(),
findByEmail: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
],
}).compile();
@@ -106,6 +143,9 @@ describe('LoginService - Zulip账号创建属性测试', () => {
zulipAccountsRepository = module.get('ZulipAccountsRepository');
apiKeySecurityService = module.get(ApiKeySecurityService);
// Mock LoginService 的 initializeZulipAdminClient 方法
jest.spyOn(loginService as any, 'initializeZulipAdminClient').mockResolvedValue(undefined);
// 设置环境变量模拟
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
@@ -167,7 +207,6 @@ describe('LoginService - Zulip账号创建属性测试', () => {
} as ZulipAccounts;
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
@@ -189,11 +228,7 @@ describe('LoginService - Zulip账号创建属性测试', () => {
expect(result.data?.is_new_user).toBe(true);
// 验证Zulip管理员客户端初始化
expect(zulipAccountService.initializeAdminClient).toHaveBeenCalledWith({
realm: 'https://test.zulip.com',
username: 'bot@test.zulip.com',
apiKey: 'test_api_key_123',
});
expect(loginService['initializeZulipAdminClient']).toHaveBeenCalled();
// 验证游戏用户注册
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
@@ -249,7 +284,6 @@ describe('LoginService - Zulip账号创建属性测试', () => {
} as Users;
// 设置模拟行为 - Zulip账号创建失败
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
@@ -318,7 +352,6 @@ describe('LoginService - Zulip账号创建属性测试', () => {
} as ZulipAccounts;
// 设置模拟行为 - 已存在Zulip账号关联
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
@@ -374,7 +407,6 @@ describe('LoginService - Zulip账号创建属性测试', () => {
} as Users;
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
@@ -404,7 +436,8 @@ describe('LoginService - Zulip账号创建属性测试', () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 设置模拟行为 - 管理员客户端初始化失败
zulipAccountService.initializeAdminClient.mockResolvedValue(false);
jest.spyOn(loginService as any, 'initializeZulipAdminClient')
.mockRejectedValue(new Error('Zulip管理员客户端初始化失败'));
// 执行注册
const result = await loginService.register(registerRequest);
@@ -418,6 +451,9 @@ describe('LoginService - Zulip账号创建属性测试', () => {
// 验证没有尝试创建Zulip账号
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
// 恢复 mock
jest.spyOn(loginService as any, 'initializeZulipAdminClient').mockResolvedValue(undefined);
}),
{ numRuns: 50 }
);
@@ -431,6 +467,10 @@ describe('LoginService - Zulip账号创建属性测试', () => {
delete process.env.ZULIP_BOT_EMAIL;
delete process.env.ZULIP_BOT_API_KEY;
// 重新设置 mock 以模拟环境变量缺失的错误
jest.spyOn(loginService as any, 'initializeZulipAdminClient')
.mockRejectedValue(new Error('Zulip管理员配置不完整请检查环境变量 ZULIP_SERVER_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY'));
// 执行注册
const result = await loginService.register(registerRequest);
@@ -441,10 +481,11 @@ describe('LoginService - Zulip账号创建属性测试', () => {
// 验证没有尝试创建游戏用户
expect(loginCoreService.register).not.toHaveBeenCalled();
// 恢复环境变量
// 恢复环境变量和 mock
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
jest.spyOn(loginService as any, 'initializeZulipAdminClient').mockResolvedValue(undefined);
}),
{ numRuns: 30 }
);
@@ -480,7 +521,6 @@ describe('LoginService - Zulip账号创建属性测试', () => {
};
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,