Merge pull request 'fix/login-verification-email-template' (#26) from fix/login-verification-email-template into main
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
22
README.md
22
README.md
@@ -78,16 +78,25 @@ pnpm run dev
|
|||||||
### 🧪 快速测试
|
### 🧪 快速测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows
|
# 运行综合测试(推荐)
|
||||||
.\test-api.ps1
|
.\test-comprehensive.ps1
|
||||||
|
|
||||||
# Linux/macOS
|
# 跳过限流测试(更快)
|
||||||
./test-api.sh
|
.\test-comprehensive.ps1 -SkipThrottleTest
|
||||||
|
|
||||||
|
# 测试远程服务器
|
||||||
|
.\test-comprehensive.ps1 -BaseUrl "https://your-server.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
**测试内容:**
|
**测试内容:**
|
||||||
|
- ✅ 应用状态检查
|
||||||
- ✅ 邮箱验证码发送与验证
|
- ✅ 邮箱验证码发送与验证
|
||||||
- ✅ 用户注册与登录
|
- ✅ 用户注册与登录
|
||||||
|
- ✅ 验证码登录功能
|
||||||
|
- ✅ 密码重置流程
|
||||||
|
- ✅ 邮箱冲突检测
|
||||||
|
- ✅ 验证码冷却时间清除
|
||||||
|
- ✅ 限流保护机制
|
||||||
- ✅ Redis文件存储功能
|
- ✅ Redis文件存储功能
|
||||||
- ✅ 邮件测试模式
|
- ✅ 邮件测试模式
|
||||||
|
|
||||||
@@ -323,9 +332,8 @@ pnpm run test:watch
|
|||||||
# 生成测试覆盖率报告
|
# 生成测试覆盖率报告
|
||||||
pnpm run test:cov
|
pnpm run test:cov
|
||||||
|
|
||||||
# API功能测试
|
# API功能测试(综合测试脚本)
|
||||||
.\test-api.ps1 # Windows
|
.\test-comprehensive.ps1
|
||||||
./test-api.sh # Linux/macOS
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📈 测试覆盖率
|
### 📈 测试覆盖率
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
3. **验证码有效期**: 所有验证码有效期为5分钟
|
3. **验证码有效期**: 所有验证码有效期为5分钟
|
||||||
4. **频率限制**: 验证码发送限制1次/分钟,注册限制10次/5分钟
|
4. **频率限制**: 验证码发送限制1次/分钟,注册限制10次/5分钟
|
||||||
5. **测试模式**: 开发环境下邮件服务返回206状态码,验证码在响应中返回
|
5. **测试模式**: 开发环境下邮件服务返回206状态码,验证码在响应中返回
|
||||||
|
6. **冷却时间自动清除**: 注册、密码重置、验证码登录成功后会自动清除验证码冷却时间,方便后续操作
|
||||||
|
7. **邮件模板修复**: 登录验证码现在使用正确的邮件模板,内容为"登录验证码"而非"密码重置"
|
||||||
|
|
||||||
### 错误处理规范
|
### 错误处理规范
|
||||||
- **409 Conflict**: 资源冲突(用户名、邮箱已存在)
|
- **409 Conflict**: 资源冲突(用户名、邮箱已存在)
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
2. 邮箱注册流程:先发送验证码 → 检查409冲突 → 使用验证码注册
|
2. 邮箱注册流程:先发送验证码 → 检查409冲突 → 使用验证码注册
|
||||||
3. 测试模式下验证码在响应中返回,生产环境需用户查收邮件
|
3. 测试模式下验证码在响应中返回,生产环境需用户查收邮件
|
||||||
4. 实现重试机制处理429频率限制错误
|
4. 实现重试机制处理429频率限制错误
|
||||||
|
5. 注册/重置密码成功后,验证码冷却时间会自动清除,可立即发送新验证码
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -945,6 +948,11 @@
|
|||||||
|
|
||||||
## 📊 版本更新记录
|
## 📊 版本更新记录
|
||||||
|
|
||||||
|
### v1.1.2 (2025-12-25)
|
||||||
|
- **验证码冷却优化**: 注册、密码重置、验证码登录成功后自动清除验证码冷却时间
|
||||||
|
- **用户体验提升**: 成功操作后可立即发送新的验证码,无需等待冷却时间
|
||||||
|
- **代码健壮性**: 冷却时间清除失败不影响主要业务流程
|
||||||
|
|
||||||
### v1.1.1 (2025-12-25)
|
### v1.1.1 (2025-12-25)
|
||||||
- **邮箱冲突检测优化**: 发送邮箱验证码前检查邮箱是否已被注册
|
- **邮箱冲突检测优化**: 发送邮箱验证码前检查邮箱是否已被注册
|
||||||
- **用户体验提升**: 避免向已注册邮箱发送无用验证码
|
- **用户体验提升**: 避免向已注册邮箱发送无用验证码
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Pixel Game Server - Auth API
|
title: Pixel Game Server - Auth API
|
||||||
description: 像素游戏服务器用户认证API接口文档 - 包含验证码登录功能和邮箱冲突检测
|
description: 像素游戏服务器用户认证API接口文档 - 包含验证码登录功能、邮箱冲突检测、验证码冷却优化和邮件模板修复
|
||||||
version: 1.1.1
|
version: 1.1.3
|
||||||
contact:
|
contact:
|
||||||
name: API Support
|
name: API Support
|
||||||
email: support@example.com
|
email: support@example.com
|
||||||
@@ -489,7 +489,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
summary: 发送登录验证码
|
summary: 发送登录验证码
|
||||||
description: 向用户邮箱或手机发送登录验证码
|
description: 向用户邮箱或手机发送登录验证码。邮件内容使用专门的登录验证码模板,标题为"登录验证码",内容说明用于登录验证而非密码重置。
|
||||||
operationId: sendLoginVerificationCode
|
operationId: sendLoginVerificationCode
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ export class LoginController {
|
|||||||
*/
|
*/
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: '发送登录验证码',
|
summary: '发送登录验证码',
|
||||||
description: '向用户邮箱或手机发送登录验证码'
|
description: '向用户邮箱或手机发送登录验证码。邮件使用专门的登录验证码模板,内容明确标识为登录验证而非密码重置。'
|
||||||
})
|
})
|
||||||
@ApiBody({ type: SendLoginVerificationCodeDto })
|
@ApiBody({ type: SendLoginVerificationCodeDto })
|
||||||
@SwaggerApiResponse({
|
@SwaggerApiResponse({
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe('LoginCoreService', () => {
|
|||||||
const mockVerificationService = {
|
const mockVerificationService = {
|
||||||
generateCode: jest.fn(),
|
generateCode: jest.fn(),
|
||||||
verifyCode: jest.fn(),
|
verifyCode: jest.fn(),
|
||||||
|
clearCooldown: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@@ -146,6 +147,69 @@ describe('LoginCoreService', () => {
|
|||||||
expect(result.isNewUser).toBe(true);
|
expect(result.isNewUser).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should register with email and clear cooldown', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
usersService.findByUsername.mockResolvedValue(null);
|
||||||
|
usersService.findByEmail.mockResolvedValue(null);
|
||||||
|
usersService.create.mockResolvedValue({ ...mockUser, email });
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockResolvedValue(undefined);
|
||||||
|
emailService.sendWelcomeEmail.mockResolvedValue(undefined);
|
||||||
|
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword');
|
||||||
|
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const result = await service.register({
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'password123',
|
||||||
|
nickname: '测试用户',
|
||||||
|
email,
|
||||||
|
email_verification_code: '123456'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.user.email).toBe(email);
|
||||||
|
expect(result.isNewUser).toBe(true);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cooldown clearing failure gracefully', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
usersService.findByUsername.mockResolvedValue(null);
|
||||||
|
usersService.findByEmail.mockResolvedValue(null);
|
||||||
|
usersService.create.mockResolvedValue({ ...mockUser, email });
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockRejectedValue(new Error('Redis error'));
|
||||||
|
emailService.sendWelcomeEmail.mockResolvedValue(undefined);
|
||||||
|
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword');
|
||||||
|
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Mock console.warn to avoid test output noise
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const result = await service.register({
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'password123',
|
||||||
|
nickname: '测试用户',
|
||||||
|
email,
|
||||||
|
email_verification_code: '123456'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.user.email).toBe(email);
|
||||||
|
expect(result.isNewUser).toBe(true);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
|
);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
`清除验证码冷却时间失败: ${email}`,
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it('should validate password strength', async () => {
|
it('should validate password strength', async () => {
|
||||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {
|
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {
|
||||||
throw new BadRequestException('密码长度至少8位');
|
throw new BadRequestException('密码长度至少8位');
|
||||||
@@ -231,6 +295,59 @@ describe('LoginCoreService', () => {
|
|||||||
expect(result).toEqual(mockUser);
|
expect(result).toEqual(mockUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reset password and clear cooldown', async () => {
|
||||||
|
const identifier = 'test@example.com';
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockResolvedValue(undefined);
|
||||||
|
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||||
|
usersService.update.mockResolvedValue(mockUser);
|
||||||
|
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||||
|
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const result = await service.resetPassword({
|
||||||
|
identifier,
|
||||||
|
verificationCode: '123456',
|
||||||
|
newPassword: 'newpassword123'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(mockUser);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.PASSWORD_RESET
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cooldown clearing failure gracefully during password reset', async () => {
|
||||||
|
const identifier = 'test@example.com';
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockRejectedValue(new Error('Redis error'));
|
||||||
|
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||||
|
usersService.update.mockResolvedValue(mockUser);
|
||||||
|
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||||
|
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Mock console.warn to avoid test output noise
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const result = await service.resetPassword({
|
||||||
|
identifier,
|
||||||
|
verificationCode: '123456',
|
||||||
|
newPassword: 'newpassword123'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(mockUser);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.PASSWORD_RESET
|
||||||
|
);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
`清除验证码冷却时间失败: ${identifier}`,
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw BadRequestException for invalid verification code', async () => {
|
it('should throw BadRequestException for invalid verification code', async () => {
|
||||||
verificationService.verifyCode.mockResolvedValue(false);
|
verificationService.verifyCode.mockResolvedValue(false);
|
||||||
|
|
||||||
@@ -336,82 +453,75 @@ describe('LoginCoreService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should successfully login with phone verification code', async () => {
|
it('should successfully login with email verification code and clear cooldown', async () => {
|
||||||
const phoneUser = { ...mockUser, phone: '+8613800138000' };
|
const identifier = 'test@example.com';
|
||||||
usersService.findAll.mockResolvedValue([phoneUser]);
|
|
||||||
verificationService.verifyCode.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const result = await service.verificationCodeLogin({
|
|
||||||
identifier: '+8613800138000',
|
|
||||||
verificationCode: '123456'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.user).toEqual(phoneUser);
|
|
||||||
expect(result.isNewUser).toBe(false);
|
|
||||||
expect(verificationService.verifyCode).toHaveBeenCalledWith(
|
|
||||||
'+8613800138000',
|
|
||||||
VerificationCodeType.SMS_VERIFICATION,
|
|
||||||
'123456'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject unverified email user', async () => {
|
|
||||||
usersService.findByEmail.mockResolvedValue(mockUser); // email_verified: false
|
|
||||||
|
|
||||||
await expect(service.verificationCodeLogin({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '123456'
|
|
||||||
})).rejects.toThrow('邮箱未验证,请先验证邮箱后再使用验证码登录');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject non-existent user', async () => {
|
|
||||||
usersService.findByEmail.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(service.verificationCodeLogin({
|
|
||||||
identifier: 'nonexistent@example.com',
|
|
||||||
verificationCode: '123456'
|
|
||||||
})).rejects.toThrow('用户不存在,请先注册账户');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject invalid verification code', async () => {
|
|
||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
|
||||||
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
|
||||||
verificationService.verifyCode.mockResolvedValue(false);
|
|
||||||
|
|
||||||
await expect(service.verificationCodeLogin({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '999999'
|
|
||||||
})).rejects.toThrow('验证码验证失败');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject invalid identifier format', async () => {
|
|
||||||
await expect(service.verificationCodeLogin({
|
|
||||||
identifier: 'invalid-identifier',
|
|
||||||
verificationCode: '123456'
|
|
||||||
})).rejects.toThrow('请提供有效的邮箱或手机号');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('verificationCodeLogin', () => {
|
|
||||||
it('should successfully login with email verification code', async () => {
|
|
||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
const verifiedUser = { ...mockUser, email_verified: true };
|
||||||
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
||||||
verificationService.verifyCode.mockResolvedValue(true);
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const result = await service.verificationCodeLogin({
|
const result = await service.verificationCodeLogin({
|
||||||
identifier: 'test@example.com',
|
identifier,
|
||||||
verificationCode: '123456'
|
verificationCode: '123456'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.user).toEqual(verifiedUser);
|
expect(result.user).toEqual(verifiedUser);
|
||||||
expect(result.isNewUser).toBe(false);
|
expect(result.isNewUser).toBe(false);
|
||||||
expect(verificationService.verifyCode).toHaveBeenCalledWith(
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
'test@example.com',
|
identifier,
|
||||||
VerificationCodeType.EMAIL_VERIFICATION,
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
'123456'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should successfully login with phone verification code and clear cooldown', async () => {
|
||||||
|
const identifier = '+8613800138000';
|
||||||
|
const phoneUser = { ...mockUser, phone: identifier };
|
||||||
|
usersService.findAll.mockResolvedValue([phoneUser]);
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await service.verificationCodeLogin({
|
||||||
|
identifier,
|
||||||
|
verificationCode: '123456'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.user).toEqual(phoneUser);
|
||||||
|
expect(result.isNewUser).toBe(false);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.SMS_VERIFICATION
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cooldown clearing failure gracefully during verification code login', async () => {
|
||||||
|
const identifier = 'test@example.com';
|
||||||
|
const verifiedUser = { ...mockUser, email_verified: true };
|
||||||
|
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
||||||
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
|
verificationService.clearCooldown.mockRejectedValue(new Error('Redis error'));
|
||||||
|
|
||||||
|
// Mock console.warn to avoid test output noise
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const result = await service.verificationCodeLogin({
|
||||||
|
identifier,
|
||||||
|
verificationCode: '123456'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.user).toEqual(verifiedUser);
|
||||||
|
expect(result.isNewUser).toBe(false);
|
||||||
|
expect(verificationService.clearCooldown).toHaveBeenCalledWith(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
|
);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
`清除验证码冷却时间失败: ${identifier}`,
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it('should successfully login with phone verification code', async () => {
|
it('should successfully login with phone verification code', async () => {
|
||||||
const phoneUser = { ...mockUser, phone: '+8613800138000' };
|
const phoneUser = { ...mockUser, phone: '+8613800138000' };
|
||||||
usersService.findAll.mockResolvedValue([phoneUser]);
|
usersService.findAll.mockResolvedValue([phoneUser]);
|
||||||
|
|||||||
@@ -239,6 +239,19 @@ export class LoginCoreService {
|
|||||||
email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证
|
email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册成功后清除验证码冷却时间,方便用户后续操作
|
||||||
|
if (email) {
|
||||||
|
try {
|
||||||
|
await this.verificationService.clearCooldown(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// 清除冷却时间失败不影响注册流程,只记录日志
|
||||||
|
console.warn(`清除验证码冷却时间失败: ${email}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果提供了邮箱,发送欢迎邮件
|
// 如果提供了邮箱,发送欢迎邮件
|
||||||
if (email) {
|
if (email) {
|
||||||
try {
|
try {
|
||||||
@@ -417,9 +430,22 @@ export class LoginCoreService {
|
|||||||
const passwordHash = await this.hashPassword(newPassword);
|
const passwordHash = await this.hashPassword(newPassword);
|
||||||
|
|
||||||
// 更新密码
|
// 更新密码
|
||||||
return await this.usersService.update(user.id, {
|
const updatedUser = await this.usersService.update(user.id, {
|
||||||
password_hash: passwordHash
|
password_hash: passwordHash
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 密码重置成功后清除验证码冷却时间
|
||||||
|
try {
|
||||||
|
await this.verificationService.clearCooldown(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.PASSWORD_RESET
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// 清除冷却时间失败不影响重置流程,只记录日志
|
||||||
|
console.warn(`清除验证码冷却时间失败: ${identifier}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -701,7 +727,15 @@ export class LoginCoreService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 验证成功,返回用户信息
|
// 5. 验证成功后清除验证码冷却时间
|
||||||
|
try {
|
||||||
|
await this.verificationService.clearCooldown(identifier, verificationType);
|
||||||
|
} catch (error) {
|
||||||
|
// 清除冷却时间失败不影响登录流程,只记录日志
|
||||||
|
console.warn(`清除验证码冷却时间失败: ${identifier}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 验证成功,返回用户信息
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
isNewUser: false
|
isNewUser: false
|
||||||
|
|||||||
@@ -221,6 +221,30 @@ describe('EmailService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('应该成功发送登录验证码', async () => {
|
||||||
|
const options: VerificationEmailOptions = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
code: '789012',
|
||||||
|
nickname: '测试用户',
|
||||||
|
purpose: 'login_verification'
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||||
|
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
|
||||||
|
|
||||||
|
const result = await service.sendVerificationCode(options);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isTestMode).toBe(false);
|
||||||
|
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
to: 'test@example.com',
|
||||||
|
subject: '【Whale Town】登录验证码',
|
||||||
|
text: '您的验证码是:789012,5分钟内有效,请勿泄露给他人。'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('应该在发送失败时返回false', async () => {
|
it('应该在发送失败时返回false', async () => {
|
||||||
const options: VerificationEmailOptions = {
|
const options: VerificationEmailOptions = {
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
@@ -323,6 +347,26 @@ describe('EmailService', () => {
|
|||||||
await service.sendVerificationCode(options);
|
await service.sendVerificationCode(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('应该生成包含验证码的登录验证模板', async () => {
|
||||||
|
const options: VerificationEmailOptions = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
code: '789012',
|
||||||
|
nickname: '测试用户',
|
||||||
|
purpose: 'login_verification'
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||||
|
expect(mailOptions.html).toContain('789012');
|
||||||
|
expect(mailOptions.html).toContain('测试用户');
|
||||||
|
expect(mailOptions.html).toContain('登录验证码');
|
||||||
|
expect(mailOptions.html).toContain('您正在使用验证码登录');
|
||||||
|
expect(mailOptions.html).toContain('🔐');
|
||||||
|
return Promise.resolve({ messageId: 'test-id' });
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.sendVerificationCode(options);
|
||||||
|
});
|
||||||
|
|
||||||
it('应该生成包含用户昵称的欢迎邮件模板', async () => {
|
it('应该生成包含用户昵称的欢迎邮件模板', async () => {
|
||||||
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||||
expect(mailOptions.html).toContain('测试用户');
|
expect(mailOptions.html).toContain('测试用户');
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export class EmailService {
|
|||||||
template = this.getPasswordResetTemplate(code, nickname);
|
template = this.getPasswordResetTemplate(code, nickname);
|
||||||
} else if (purpose === 'login_verification') {
|
} else if (purpose === 'login_verification') {
|
||||||
subject = '【Whale Town】登录验证码';
|
subject = '【Whale Town】登录验证码';
|
||||||
template = this.getPasswordResetTemplate(code, nickname); // 复用密码重置模板
|
template = this.getLoginVerificationTemplate(code, nickname);
|
||||||
} else {
|
} else {
|
||||||
subject = '【Whale Town】验证码';
|
subject = '【Whale Town】验证码';
|
||||||
template = this.getEmailVerificationTemplate(code, nickname);
|
template = this.getEmailVerificationTemplate(code, nickname);
|
||||||
|
|||||||
@@ -587,4 +587,69 @@ describe('VerificationService', () => {
|
|||||||
expect(code).toMatch(/^\d{6}$/);
|
expect(code).toMatch(/^\d{6}$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clearCooldown', () => {
|
||||||
|
it('应该成功清除验证码冷却时间', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const type = VerificationCodeType.EMAIL_VERIFICATION;
|
||||||
|
|
||||||
|
mockRedis.del.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await service.clearCooldown(email, type);
|
||||||
|
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理清除不存在的冷却时间', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const type = VerificationCodeType.EMAIL_VERIFICATION;
|
||||||
|
|
||||||
|
mockRedis.del.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await service.clearCooldown(email, type);
|
||||||
|
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理Redis删除操作错误', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const type = VerificationCodeType.EMAIL_VERIFICATION;
|
||||||
|
|
||||||
|
mockRedis.del.mockRejectedValue(new Error('Redis delete failed'));
|
||||||
|
|
||||||
|
await expect(service.clearCooldown(email, type)).rejects.toThrow('Redis delete failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该为不同类型的验证码清除对应的冷却时间', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const types = [
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION,
|
||||||
|
VerificationCodeType.PASSWORD_RESET,
|
||||||
|
VerificationCodeType.SMS_VERIFICATION,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRedis.del.mockResolvedValue(true);
|
||||||
|
|
||||||
|
for (const type of types) {
|
||||||
|
await service.clearCooldown(email, type);
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledTimes(types.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该为不同标识符清除对应的冷却时间', async () => {
|
||||||
|
const identifiers = ['test1@example.com', 'test2@example.com', '+8613800138000'];
|
||||||
|
const type = VerificationCodeType.EMAIL_VERIFICATION;
|
||||||
|
|
||||||
|
mockRedis.del.mockResolvedValue(true);
|
||||||
|
|
||||||
|
for (const identifier of identifiers) {
|
||||||
|
await service.clearCooldown(identifier, type);
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${identifier}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockRedis.del).toHaveBeenCalledTimes(identifiers.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -294,6 +294,18 @@ export class VerificationService {
|
|||||||
return `verification_hourly:${type}:${identifier}:${date}:${hour}`;
|
return `verification_hourly:${type}:${identifier}:${date}:${hour}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除验证码冷却时间
|
||||||
|
*
|
||||||
|
* @param identifier 标识符
|
||||||
|
* @param type 验证码类型
|
||||||
|
*/
|
||||||
|
async clearCooldown(identifier: string, type: VerificationCodeType): Promise<void> {
|
||||||
|
const cooldownKey = this.buildCooldownKey(identifier, type);
|
||||||
|
await this.redis.del(cooldownKey);
|
||||||
|
this.logger.log(`验证码冷却时间已清除: ${identifier} (${type})`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期的验证码(可选的定时任务)
|
* 清理过期的验证码(可选的定时任务)
|
||||||
*/
|
*/
|
||||||
|
|||||||
93
test-api.ps1
93
test-api.ps1
@@ -1,93 +0,0 @@
|
|||||||
# Whale Town API Test Script (Windows PowerShell)
|
|
||||||
# 测试邮箱验证码和用户注册登录功能
|
|
||||||
|
|
||||||
param(
|
|
||||||
[string]$BaseUrl = "http://localhost:3000",
|
|
||||||
[string]$TestEmail = "test@example.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-Host "=== Whale Town API Test (Windows) ===" -ForegroundColor Green
|
|
||||||
Write-Host "Testing without database and email server" -ForegroundColor Cyan
|
|
||||||
Write-Host "Base URL: $BaseUrl" -ForegroundColor Yellow
|
|
||||||
Write-Host "Test Email: $TestEmail" -ForegroundColor Yellow
|
|
||||||
|
|
||||||
# Test 1: Send verification code
|
|
||||||
Write-Host "`n1. Sending email verification code..." -ForegroundColor Yellow
|
|
||||||
$sendBody = @{
|
|
||||||
email = $TestEmail
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sendResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "✅ Verification code sent successfully" -ForegroundColor Green
|
|
||||||
Write-Host " Code: $($sendResponse.data.verification_code)" -ForegroundColor Cyan
|
|
||||||
Write-Host " Test Mode: $($sendResponse.data.is_test_mode)" -ForegroundColor Cyan
|
|
||||||
$verificationCode = $sendResponse.data.verification_code
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ Failed to send verification code" -ForegroundColor Red
|
|
||||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 2: Verify email code
|
|
||||||
Write-Host "`n2. Verifying email code..." -ForegroundColor Yellow
|
|
||||||
$verifyBody = @{
|
|
||||||
email = $TestEmail
|
|
||||||
verification_code = $verificationCode
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$verifyResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/verify-email" -Method POST -Body $verifyBody -ContentType "application/json"
|
|
||||||
Write-Host "✅ Email verification successful" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ Email verification failed" -ForegroundColor Red
|
|
||||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 3: User registration
|
|
||||||
Write-Host "`n3. Testing user registration..." -ForegroundColor Yellow
|
|
||||||
$registerBody = @{
|
|
||||||
username = "testuser_$(Get-Random -Maximum 9999)"
|
|
||||||
password = "Test123456"
|
|
||||||
nickname = "Test User"
|
|
||||||
email = $TestEmail
|
|
||||||
email_verification_code = $verificationCode
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$registerResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/register" -Method POST -Body $registerBody -ContentType "application/json"
|
|
||||||
Write-Host "✅ User registration successful" -ForegroundColor Green
|
|
||||||
Write-Host " User ID: $($registerResponse.data.user.id)" -ForegroundColor Cyan
|
|
||||||
Write-Host " Username: $($registerResponse.data.user.username)" -ForegroundColor Cyan
|
|
||||||
$username = $registerResponse.data.user.username
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ User registration failed" -ForegroundColor Red
|
|
||||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
$username = $null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 4: User login
|
|
||||||
if ($username) {
|
|
||||||
Write-Host "`n4. Testing user login..." -ForegroundColor Yellow
|
|
||||||
$loginBody = @{
|
|
||||||
identifier = $username
|
|
||||||
password = "Test123456"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$loginResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/login" -Method POST -Body $loginBody -ContentType "application/json"
|
|
||||||
Write-Host "✅ User login successful" -ForegroundColor Green
|
|
||||||
Write-Host " Username: $($loginResponse.data.user.username)" -ForegroundColor Cyan
|
|
||||||
Write-Host " Nickname: $($loginResponse.data.user.nickname)" -ForegroundColor Cyan
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ User login failed" -ForegroundColor Red
|
|
||||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n=== Test Summary ===" -ForegroundColor Green
|
|
||||||
Write-Host "✅ Redis file storage: Working" -ForegroundColor Green
|
|
||||||
Write-Host "✅ Email test mode: Working" -ForegroundColor Green
|
|
||||||
Write-Host "✅ Memory user storage: Working" -ForegroundColor Green
|
|
||||||
Write-Host "`n💡 Check redis-data/redis.json for stored verification data" -ForegroundColor Yellow
|
|
||||||
Write-Host "💡 Check server console for email content output" -ForegroundColor Yellow
|
|
||||||
95
test-api.sh
95
test-api.sh
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Whale Town API Test Script (Linux/macOS)
|
|
||||||
# 测试邮箱验证码和用户注册登录功能
|
|
||||||
|
|
||||||
BASE_URL="${1:-http://localhost:3000}"
|
|
||||||
TEST_EMAIL="${2:-test@example.com}"
|
|
||||||
|
|
||||||
echo "=== Whale Town API Test (Linux/macOS) ==="
|
|
||||||
echo "Testing without database and email server"
|
|
||||||
echo "Base URL: $BASE_URL"
|
|
||||||
echo "Test Email: $TEST_EMAIL"
|
|
||||||
|
|
||||||
# Test 1: Send verification code
|
|
||||||
echo ""
|
|
||||||
echo "1. Sending email verification code..."
|
|
||||||
SEND_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/send-email-verification" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"email\":\"$TEST_EMAIL\"}")
|
|
||||||
|
|
||||||
if echo "$SEND_RESPONSE" | grep -q '"success"'; then
|
|
||||||
echo "✅ Verification code sent successfully"
|
|
||||||
VERIFICATION_CODE=$(echo "$SEND_RESPONSE" | grep -o '"verification_code":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
IS_TEST_MODE=$(echo "$SEND_RESPONSE" | grep -o '"is_test_mode":[^,}]*' | cut -d':' -f2)
|
|
||||||
echo " Code: $VERIFICATION_CODE"
|
|
||||||
echo " Test Mode: $IS_TEST_MODE"
|
|
||||||
else
|
|
||||||
echo "❌ Failed to send verification code"
|
|
||||||
echo " Response: $SEND_RESPONSE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 2: Verify email code
|
|
||||||
echo ""
|
|
||||||
echo "2. Verifying email code..."
|
|
||||||
VERIFY_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/verify-email" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"email\":\"$TEST_EMAIL\",\"verification_code\":\"$VERIFICATION_CODE\"}")
|
|
||||||
|
|
||||||
if echo "$VERIFY_RESPONSE" | grep -q '"success":true'; then
|
|
||||||
echo "✅ Email verification successful"
|
|
||||||
else
|
|
||||||
echo "❌ Email verification failed"
|
|
||||||
echo " Response: $VERIFY_RESPONSE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 3: User registration
|
|
||||||
echo ""
|
|
||||||
echo "3. Testing user registration..."
|
|
||||||
RANDOM_NUM=$((RANDOM % 9999))
|
|
||||||
USERNAME="testuser_$RANDOM_NUM"
|
|
||||||
|
|
||||||
REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/register" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\",\"password\":\"Test123456\",\"nickname\":\"Test User\",\"email\":\"$TEST_EMAIL\",\"email_verification_code\":\"$VERIFICATION_CODE\"}")
|
|
||||||
|
|
||||||
if echo "$REGISTER_RESPONSE" | grep -q '"success":true'; then
|
|
||||||
echo "✅ User registration successful"
|
|
||||||
USER_ID=$(echo "$REGISTER_RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
REGISTERED_USERNAME=$(echo "$REGISTER_RESPONSE" | grep -o '"username":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
echo " User ID: $USER_ID"
|
|
||||||
echo " Username: $REGISTERED_USERNAME"
|
|
||||||
else
|
|
||||||
echo "❌ User registration failed"
|
|
||||||
echo " Response: $REGISTER_RESPONSE"
|
|
||||||
REGISTERED_USERNAME=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 4: User login
|
|
||||||
if [ -n "$REGISTERED_USERNAME" ]; then
|
|
||||||
echo ""
|
|
||||||
echo "4. Testing user login..."
|
|
||||||
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"identifier\":\"$REGISTERED_USERNAME\",\"password\":\"Test123456\"}")
|
|
||||||
|
|
||||||
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
|
|
||||||
echo "✅ User login successful"
|
|
||||||
LOGIN_USERNAME=$(echo "$LOGIN_RESPONSE" | grep -o '"username":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
LOGIN_NICKNAME=$(echo "$LOGIN_RESPONSE" | grep -o '"nickname":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
echo " Username: $LOGIN_USERNAME"
|
|
||||||
echo " Nickname: $LOGIN_NICKNAME"
|
|
||||||
else
|
|
||||||
echo "❌ User login failed"
|
|
||||||
echo " Response: $LOGIN_RESPONSE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Summary ==="
|
|
||||||
echo "✅ Redis file storage: Working"
|
|
||||||
echo "✅ Email test mode: Working"
|
|
||||||
echo "✅ Memory user storage: Working"
|
|
||||||
echo ""
|
|
||||||
echo "💡 Check redis-data/redis.json for stored verification data"
|
|
||||||
echo "💡 Check server console for email content output"
|
|
||||||
333
test-comprehensive.ps1
Normal file
333
test-comprehensive.ps1
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# Comprehensive API Test Script
|
||||||
|
# 综合API测试脚本 - 完整的后端功能测试
|
||||||
|
#
|
||||||
|
# 🧪 测试内容:
|
||||||
|
# 1. 基础API功能(应用状态、注册、登录)
|
||||||
|
# 2. 邮箱验证码流程(发送、验证、冲突检测)
|
||||||
|
# 3. 验证码冷却时间清除功能
|
||||||
|
# 4. 限流保护机制
|
||||||
|
# 5. 密码重置流程
|
||||||
|
# 6. 验证码登录功能
|
||||||
|
# 7. 错误处理和边界条件
|
||||||
|
#
|
||||||
|
# 🚀 使用方法:
|
||||||
|
# .\test-comprehensive.ps1 # 运行完整测试
|
||||||
|
# .\test-comprehensive.ps1 -SkipThrottleTest # 跳过限流测试
|
||||||
|
# .\test-comprehensive.ps1 -SkipCooldownTest # 跳过冷却测试
|
||||||
|
# .\test-comprehensive.ps1 -BaseUrl "https://your-server.com" # 测试远程服务器
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$BaseUrl = "http://localhost:3000",
|
||||||
|
[switch]$SkipThrottleTest = $false,
|
||||||
|
[switch]$SkipCooldownTest = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
|
Write-Host "🧪 Comprehensive API Test Suite" -ForegroundColor Green
|
||||||
|
Write-Host "===============================" -ForegroundColor Green
|
||||||
|
Write-Host "Base URL: $BaseUrl" -ForegroundColor Yellow
|
||||||
|
Write-Host "Skip Throttle Test: $SkipThrottleTest" -ForegroundColor Yellow
|
||||||
|
Write-Host "Skip Cooldown Test: $SkipCooldownTest" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Helper function to handle API responses
|
||||||
|
function Test-ApiCall {
|
||||||
|
param(
|
||||||
|
[string]$TestName,
|
||||||
|
[string]$Url,
|
||||||
|
[string]$Body,
|
||||||
|
[string]$Method = "POST",
|
||||||
|
[int]$ExpectedStatus = 200,
|
||||||
|
[switch]$Silent = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Silent) {
|
||||||
|
Write-Host "`n📋 $TestName" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri $Url -Method $Method -Body $Body -ContentType "application/json" -ErrorAction Stop
|
||||||
|
if (-not $Silent) {
|
||||||
|
Write-Host "✅ SUCCESS ($(if ($response.success) { 'true' } else { 'false' }))" -ForegroundColor Green
|
||||||
|
Write-Host "Message: $($response.message)" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
return $response
|
||||||
|
} catch {
|
||||||
|
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||||
|
if (-not $Silent) {
|
||||||
|
Write-Host "❌ FAILED ($statusCode)" -ForegroundColor $(if ($statusCode -eq $ExpectedStatus) { "Yellow" } else { "Red" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_.Exception.Response) {
|
||||||
|
$stream = $_.Exception.Response.GetResponseStream()
|
||||||
|
$reader = New-Object System.IO.StreamReader($stream)
|
||||||
|
$responseBody = $reader.ReadToEnd()
|
||||||
|
$reader.Close()
|
||||||
|
$stream.Close()
|
||||||
|
|
||||||
|
if ($responseBody) {
|
||||||
|
try {
|
||||||
|
$errorResponse = $responseBody | ConvertFrom-Json
|
||||||
|
if (-not $Silent) {
|
||||||
|
Write-Host "Message: $($errorResponse.message)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Error Code: $($errorResponse.error_code)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
return $errorResponse
|
||||||
|
} catch {
|
||||||
|
if (-not $Silent) {
|
||||||
|
Write-Host "Raw Response: $responseBody" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear throttle first
|
||||||
|
Write-Host "`n🔄 Clearing throttle records..." -ForegroundColor Blue
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$BaseUrl/auth/debug-clear-throttle" -Method POST | Out-Null
|
||||||
|
Write-Host "✅ Throttle cleared" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "⚠️ Could not clear throttle" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test Results Tracking
|
||||||
|
$testResults = @{
|
||||||
|
AppStatus = $false
|
||||||
|
BasicAPI = $false
|
||||||
|
EmailConflict = $false
|
||||||
|
VerificationCodeLogin = $false
|
||||||
|
CooldownClearing = $false
|
||||||
|
ThrottleProtection = $false
|
||||||
|
PasswordReset = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 0: Application Status" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Test application status
|
||||||
|
$result0 = Test-ApiCall -TestName "Check application status" -Url "$BaseUrl" -Method "GET" -Body ""
|
||||||
|
|
||||||
|
if ($result0 -and $result0.service -eq "Pixel Game Server") {
|
||||||
|
$testResults.AppStatus = $true
|
||||||
|
Write-Host "✅ PASS: Application is running" -ForegroundColor Green
|
||||||
|
Write-Host " Service: $($result0.service)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Version: $($result0.version)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Environment: $($result0.environment)" -ForegroundColor Cyan
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Application status check failed" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 1: Basic API Functionality" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Generate unique test data
|
||||||
|
$testEmail = "comprehensive_test_$(Get-Random)@example.com"
|
||||||
|
$testUsername = "comp_test_$(Get-Random)"
|
||||||
|
|
||||||
|
# Test 1: Send verification code
|
||||||
|
$result1 = Test-ApiCall -TestName "Send email verification code" -Url "$BaseUrl/auth/send-email-verification" -Body (@{
|
||||||
|
email = $testEmail
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result1 -and $result1.data.verification_code) {
|
||||||
|
$verificationCode = $result1.data.verification_code
|
||||||
|
Write-Host "Got verification code: $verificationCode" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Test 2: Register user
|
||||||
|
$result2 = Test-ApiCall -TestName "Register new user" -Url "$BaseUrl/auth/register" -Body (@{
|
||||||
|
username = $testUsername
|
||||||
|
password = "password123"
|
||||||
|
nickname = "Comprehensive Test User"
|
||||||
|
email = $testEmail
|
||||||
|
email_verification_code = $verificationCode
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result2 -and $result2.success) {
|
||||||
|
# Test 3: Login user
|
||||||
|
$result3 = Test-ApiCall -TestName "Login with registered user" -Url "$BaseUrl/auth/login" -Body (@{
|
||||||
|
identifier = $testUsername
|
||||||
|
password = "password123"
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result3 -and $result3.success) {
|
||||||
|
$testResults.BasicAPI = $true
|
||||||
|
Write-Host "✅ PASS: Basic API functionality working" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 2: Email Conflict Detection" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Test email conflict detection
|
||||||
|
$result4 = Test-ApiCall -TestName "Test email conflict detection" -Url "$BaseUrl/auth/send-email-verification" -Body (@{
|
||||||
|
email = $testEmail
|
||||||
|
} | ConvertTo-Json) -ExpectedStatus 409
|
||||||
|
|
||||||
|
if ($result4 -and $result4.message -like "*已被注册*") {
|
||||||
|
$testResults.EmailConflict = $true
|
||||||
|
Write-Host "✅ PASS: Email conflict detection working" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Email conflict detection not working" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 3: Verification Code Login" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Test verification code login
|
||||||
|
if ($result2 -and $result2.success) {
|
||||||
|
$userEmail = $result2.data.user.email
|
||||||
|
|
||||||
|
# Send login verification code
|
||||||
|
$result4a = Test-ApiCall -TestName "Send login verification code" -Url "$BaseUrl/auth/send-login-verification-code" -Body (@{
|
||||||
|
identifier = $userEmail
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result4a -and $result4a.data.verification_code) {
|
||||||
|
$loginCode = $result4a.data.verification_code
|
||||||
|
|
||||||
|
# Login with verification code
|
||||||
|
$result4b = Test-ApiCall -TestName "Login with verification code" -Url "$BaseUrl/auth/verification-code-login" -Body (@{
|
||||||
|
identifier = $userEmail
|
||||||
|
verification_code = $loginCode
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result4b -and $result4b.success) {
|
||||||
|
$testResults.VerificationCodeLogin = $true
|
||||||
|
Write-Host "✅ PASS: Verification code login working" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Verification code login failed" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipCooldownTest) {
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 4: Cooldown Clearing & Password Reset" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Test cooldown clearing with password reset
|
||||||
|
if ($result2 -and $result2.success) {
|
||||||
|
$userEmail = $result2.data.user.email
|
||||||
|
|
||||||
|
# Send password reset code
|
||||||
|
$result5 = Test-ApiCall -TestName "Send password reset code" -Url "$BaseUrl/auth/forgot-password" -Body (@{
|
||||||
|
identifier = $userEmail
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result5 -and $result5.data.verification_code) {
|
||||||
|
$resetCode = $result5.data.verification_code
|
||||||
|
|
||||||
|
# Reset password
|
||||||
|
$result6 = Test-ApiCall -TestName "Reset password (should clear cooldown)" -Url "$BaseUrl/auth/reset-password" -Body (@{
|
||||||
|
identifier = $userEmail
|
||||||
|
verification_code = $resetCode
|
||||||
|
new_password = "newpassword123"
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result6 -and $result6.success) {
|
||||||
|
$testResults.PasswordReset = $true
|
||||||
|
Write-Host "✅ PASS: Password reset working" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Test immediate code sending (should work if cooldown cleared)
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
$result7 = Test-ApiCall -TestName "Send reset code immediately (test cooldown clearing)" -Url "$BaseUrl/auth/forgot-password" -Body (@{
|
||||||
|
identifier = $userEmail
|
||||||
|
} | ConvertTo-Json)
|
||||||
|
|
||||||
|
if ($result7 -and $result7.success) {
|
||||||
|
$testResults.CooldownClearing = $true
|
||||||
|
Write-Host "✅ PASS: Cooldown clearing working" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Cooldown not cleared properly" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Password reset failed" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipThrottleTest) {
|
||||||
|
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||||
|
Write-Host "🧪 Test Suite 5: Throttle Protection" -ForegroundColor Cyan
|
||||||
|
Write-Host "="*60 -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$successCount = 0
|
||||||
|
$throttleCount = 0
|
||||||
|
|
||||||
|
Write-Host "Testing throttle limits (making 12 registration requests)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
for ($i = 1; $i -le 12; $i++) {
|
||||||
|
$result = Test-ApiCall -TestName "Registration attempt $i" -Url "$BaseUrl/auth/register" -Body (@{
|
||||||
|
username = "throttle_test_$i"
|
||||||
|
password = "password123"
|
||||||
|
nickname = "Throttle Test $i"
|
||||||
|
} | ConvertTo-Json) -Silent
|
||||||
|
|
||||||
|
if ($result -and $result.success) {
|
||||||
|
$successCount++
|
||||||
|
Write-Host " Request $i`: ✅ Success" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$throttleCount++
|
||||||
|
Write-Host " Request $i`: 🚦 Throttled" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nThrottle Results: $successCount success, $throttleCount throttled" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if ($successCount -ge 8 -and $throttleCount -ge 1) {
|
||||||
|
$testResults.ThrottleProtection = $true
|
||||||
|
Write-Host "✅ PASS: Throttle protection working" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ FAIL: Throttle protection not working properly" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n🎯 Test Results Summary" -ForegroundColor Green
|
||||||
|
Write-Host "=======================" -ForegroundColor Green
|
||||||
|
|
||||||
|
$passCount = 0
|
||||||
|
$totalTests = 0
|
||||||
|
|
||||||
|
foreach ($test in $testResults.GetEnumerator()) {
|
||||||
|
$totalTests++
|
||||||
|
if ($test.Value) {
|
||||||
|
$passCount++
|
||||||
|
Write-Host "✅ $($test.Key): PASS" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ $($test.Key): FAIL" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n📊 Overall Result: $passCount/$totalTests tests passed" -ForegroundColor $(if ($passCount -eq $totalTests) { "Green" } else { "Yellow" })
|
||||||
|
|
||||||
|
if ($passCount -eq $totalTests) {
|
||||||
|
Write-Host "🎉 All tests passed! API is working correctly." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Some tests failed. Please check the implementation." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n💡 Usage Tips:" -ForegroundColor Cyan
|
||||||
|
Write-Host " • Use -SkipThrottleTest to skip throttle testing" -ForegroundColor White
|
||||||
|
Write-Host " • Use -SkipCooldownTest to skip cooldown testing" -ForegroundColor White
|
||||||
|
Write-Host " • Check server logs for detailed error information" -ForegroundColor White
|
||||||
|
Write-Host " • For production testing: .\test-comprehensive.ps1 -BaseUrl 'https://your-server.com'" -ForegroundColor White
|
||||||
|
|
||||||
|
Write-Host "`n📋 Test Coverage:" -ForegroundColor Cyan
|
||||||
|
Write-Host " ✓ Application Status & Health Check" -ForegroundColor White
|
||||||
|
Write-Host " ✓ User Registration & Login Flow" -ForegroundColor White
|
||||||
|
Write-Host " ✓ Email Verification & Conflict Detection" -ForegroundColor White
|
||||||
|
Write-Host " ✓ Verification Code Login" -ForegroundColor White
|
||||||
|
Write-Host " ✓ Password Reset Flow" -ForegroundColor White
|
||||||
|
Write-Host " ✓ Cooldown Time Clearing" -ForegroundColor White
|
||||||
|
Write-Host " ✓ Rate Limiting & Throttle Protection" -ForegroundColor White
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# Test register API fix - Core functionality test
|
|
||||||
# 测试注册API修复 - 核心功能测试
|
|
||||||
#
|
|
||||||
# 主要测试内容:
|
|
||||||
# 1. 用户注册(无邮箱)- 应该成功
|
|
||||||
# 2. 用户注册(有邮箱但无验证码)- 应该失败并返回正确错误信息
|
|
||||||
# 3. 用户存在性检查 - 应该在验证码验证之前进行,返回"用户名已存在"
|
|
||||||
# 4. 邮箱验证码完整流程 - 验证码生成、注册、重复邮箱检查
|
|
||||||
# 5. 邮箱冲突检测 - 发送验证码前检查邮箱是否已注册
|
|
||||||
#
|
|
||||||
# 修复验证:
|
|
||||||
# - 用户存在检查现在在验证码验证之前执行
|
|
||||||
# - 邮箱冲突检测防止向已注册邮箱发送验证码
|
|
||||||
# - 验证码不会因为用户已存在而被无效消费
|
|
||||||
# - 错误信息更加准确和用户友好
|
|
||||||
# - 返回正确的HTTP状态码(409 Conflict)
|
|
||||||
$baseUrl = "http://localhost:3000"
|
|
||||||
|
|
||||||
Write-Host "🧪 Testing Register API Fix" -ForegroundColor Green
|
|
||||||
Write-Host "============================" -ForegroundColor Green
|
|
||||||
|
|
||||||
# Helper function to handle API responses
|
|
||||||
function Test-ApiCall {
|
|
||||||
param(
|
|
||||||
[string]$TestName,
|
|
||||||
[string]$Url,
|
|
||||||
[string]$Body,
|
|
||||||
[int]$ExpectedStatus = 200
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-Host "`n📋 $TestName" -ForegroundColor Yellow
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Invoke-RestMethod -Uri $Url -Method POST -Body $Body -ContentType "application/json" -ErrorAction Stop
|
|
||||||
Write-Host "✅ SUCCESS ($(if ($response.success) { 'true' } else { 'false' }))" -ForegroundColor Green
|
|
||||||
Write-Host "Message: $($response.message)" -ForegroundColor Cyan
|
|
||||||
return $response
|
|
||||||
} catch {
|
|
||||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
|
||||||
Write-Host "❌ FAILED ($statusCode)" -ForegroundColor $(if ($statusCode -eq $ExpectedStatus) { "Yellow" } else { "Red" })
|
|
||||||
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$stream = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($stream)
|
|
||||||
$responseBody = $reader.ReadToEnd()
|
|
||||||
$reader.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
if ($responseBody) {
|
|
||||||
try {
|
|
||||||
$errorResponse = $responseBody | ConvertFrom-Json
|
|
||||||
Write-Host "Message: $($errorResponse.message)" -ForegroundColor Cyan
|
|
||||||
Write-Host "Error Code: $($errorResponse.error_code)" -ForegroundColor Gray
|
|
||||||
return $errorResponse
|
|
||||||
} catch {
|
|
||||||
Write-Host "Raw Response: $responseBody" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "Empty response body" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear throttle first
|
|
||||||
Write-Host "`n🔄 Clearing throttle records..." -ForegroundColor Blue
|
|
||||||
try {
|
|
||||||
Invoke-RestMethod -Uri "$baseUrl/auth/debug-clear-throttle" -Method POST | Out-Null
|
|
||||||
Write-Host "✅ Throttle cleared" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "⚠️ Could not clear throttle" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 1: Register without email (should succeed)
|
|
||||||
$result1 = Test-ApiCall -TestName "Register without email" -Url "$baseUrl/auth/register" -Body (@{
|
|
||||||
username = "testuser_$(Get-Random)"
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Test User"
|
|
||||||
} | ConvertTo-Json)
|
|
||||||
|
|
||||||
# Test 2: Register with email but no verification code (should fail)
|
|
||||||
$result2 = Test-ApiCall -TestName "Register with email but no verification code" -Url "$baseUrl/auth/register" -Body (@{
|
|
||||||
username = "testuser_$(Get-Random)"
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Test User"
|
|
||||||
email = "test@example.com"
|
|
||||||
} | ConvertTo-Json) -ExpectedStatus 400
|
|
||||||
|
|
||||||
# Test 3: Try to register with existing username (should fail with correct error)
|
|
||||||
if ($result1 -and $result1.success) {
|
|
||||||
$existingUsername = ($result1.data.user.username)
|
|
||||||
$result3 = Test-ApiCall -TestName "Register with existing username ($existingUsername)" -Url "$baseUrl/auth/register" -Body (@{
|
|
||||||
username = $existingUsername
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Duplicate User"
|
|
||||||
} | ConvertTo-Json) -ExpectedStatus 400
|
|
||||||
|
|
||||||
if ($result3 -and $result3.message -like "*用户名已存在*") {
|
|
||||||
Write-Host "✅ PASS: Correct error message for existing user" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "❌ FAIL: Wrong error message for existing user" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 4: Email conflict detection test
|
|
||||||
Write-Host "`n📋 Email conflict detection test" -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
# First, try to get verification code for a new email
|
|
||||||
$newEmail = "newuser_$(Get-Random)@test.com"
|
|
||||||
$emailResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body (@{email = $newEmail} | ConvertTo-Json) -ContentType "application/json"
|
|
||||||
|
|
||||||
if ($emailResponse.data.verification_code) {
|
|
||||||
$verificationCode = $emailResponse.data.verification_code
|
|
||||||
Write-Host "Got verification code: $verificationCode" -ForegroundColor Green
|
|
||||||
|
|
||||||
# Register user with this email
|
|
||||||
$result4 = Test-ApiCall -TestName "Register with valid email and verification code" -Url "$baseUrl/auth/register" -Body (@{
|
|
||||||
username = "emailuser_$(Get-Random)"
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Email User"
|
|
||||||
email = $newEmail
|
|
||||||
email_verification_code = $verificationCode
|
|
||||||
} | ConvertTo-Json)
|
|
||||||
|
|
||||||
if ($result4 -and $result4.success) {
|
|
||||||
Write-Host "✅ PASS: Email registration successful" -ForegroundColor Green
|
|
||||||
|
|
||||||
# Now test email conflict detection
|
|
||||||
Write-Host "`n📋 Testing email conflict detection" -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
$conflictResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body (@{email = $newEmail} | ConvertTo-Json) -ContentType "application/json"
|
|
||||||
Write-Host "❌ FAIL: Should have detected email conflict" -ForegroundColor Red
|
|
||||||
} catch {
|
|
||||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
|
||||||
if ($statusCode -eq 409) {
|
|
||||||
Write-Host "✅ PASS: Email conflict detected (409 status)" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "❌ FAIL: Wrong status code for email conflict ($statusCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "⚠️ Could not test email verification (email service may not be configured)" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n🎯 Test Summary" -ForegroundColor Green
|
|
||||||
Write-Host "===============" -ForegroundColor Green
|
|
||||||
Write-Host "✅ Registration logic has been fixed:" -ForegroundColor White
|
|
||||||
Write-Host " • User existence checked BEFORE verification code validation" -ForegroundColor White
|
|
||||||
Write-Host " • Email conflict detection prevents sending codes to registered emails" -ForegroundColor White
|
|
||||||
Write-Host " • Proper error messages for different scenarios" -ForegroundColor White
|
|
||||||
Write-Host " • Verification codes not wasted on existing users" -ForegroundColor White
|
|
||||||
Write-Host " • Returns 409 Conflict for email/username conflicts" -ForegroundColor White
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# Test throttle functionality
|
|
||||||
# 测试限流功能
|
|
||||||
#
|
|
||||||
# 主要测试内容:
|
|
||||||
# 1. 限流记录清除功能
|
|
||||||
# 2. 正常注册请求(在限流范围内)
|
|
||||||
# 3. 批量请求测试限流阈值
|
|
||||||
# 4. 验证限流配置是否正确生效
|
|
||||||
#
|
|
||||||
# 当前限流配置:
|
|
||||||
# - 注册接口:10次/5分钟(开发环境已放宽)
|
|
||||||
# - 登录接口:5次/分钟
|
|
||||||
# - 发送验证码:1次/分钟
|
|
||||||
# - 密码重置:3次/小时
|
|
||||||
$baseUrl = "http://localhost:3000"
|
|
||||||
|
|
||||||
Write-Host "🚦 Testing Throttle Functionality" -ForegroundColor Green
|
|
||||||
Write-Host "==================================" -ForegroundColor Green
|
|
||||||
|
|
||||||
# Clear throttle first
|
|
||||||
Write-Host "`n🔄 Clearing throttle records..." -ForegroundColor Blue
|
|
||||||
try {
|
|
||||||
$clearResponse = Invoke-RestMethod -Uri "$baseUrl/auth/debug-clear-throttle" -Method POST
|
|
||||||
Write-Host "✅ $($clearResponse.message)" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "⚠️ Could not clear throttle records" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test normal registration (should work with increased limit)
|
|
||||||
Write-Host "`n📋 Test 1: Normal registration with increased throttle limit" -ForegroundColor Yellow
|
|
||||||
$registerData = @{
|
|
||||||
username = "testuser_throttle_$(Get-Random)"
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Test User Throttle"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Invoke-RestMethod -Uri "$baseUrl/auth/register" -Method POST -Body $registerData -ContentType "application/json" -ErrorAction Stop
|
|
||||||
Write-Host "✅ SUCCESS: Registration completed" -ForegroundColor Green
|
|
||||||
Write-Host "Message: $($response.message)" -ForegroundColor Cyan
|
|
||||||
} catch {
|
|
||||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
|
||||||
Write-Host "❌ FAILED ($statusCode)" -ForegroundColor $(if ($statusCode -eq 429) { "Yellow" } else { "Red" })
|
|
||||||
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
|
||||||
$responseBody = $reader.ReadToEnd()
|
|
||||||
$reader.Close()
|
|
||||||
|
|
||||||
try {
|
|
||||||
$errorResponse = $responseBody | ConvertFrom-Json
|
|
||||||
Write-Host "Message: $($errorResponse.message)" -ForegroundColor Cyan
|
|
||||||
if ($errorResponse.throttle_info) {
|
|
||||||
Write-Host "Throttle Info:" -ForegroundColor Gray
|
|
||||||
Write-Host " Limit: $($errorResponse.throttle_info.limit)" -ForegroundColor Gray
|
|
||||||
Write-Host " Window: $($errorResponse.throttle_info.window_seconds)s" -ForegroundColor Gray
|
|
||||||
Write-Host " Current: $($errorResponse.throttle_info.current_requests)" -ForegroundColor Gray
|
|
||||||
Write-Host " Reset: $($errorResponse.throttle_info.reset_time)" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "Raw Response: $responseBody" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test throttle limits by making multiple requests
|
|
||||||
Write-Host "`n📋 Test 2: Testing throttle limits (register endpoint: 10 requests/5min)" -ForegroundColor Yellow
|
|
||||||
$successCount = 0
|
|
||||||
$throttleCount = 0
|
|
||||||
|
|
||||||
for ($i = 1; $i -le 12; $i++) {
|
|
||||||
$testData = @{
|
|
||||||
username = "throttletest_$i"
|
|
||||||
password = "password123"
|
|
||||||
nickname = "Throttle Test $i"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Invoke-RestMethod -Uri "$baseUrl/auth/register" -Method POST -Body $testData -ContentType "application/json" -ErrorAction Stop
|
|
||||||
$successCount++
|
|
||||||
Write-Host " Request $i`: ✅ Success" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
|
||||||
if ($statusCode -eq 429) {
|
|
||||||
$throttleCount++
|
|
||||||
Write-Host " Request $i`: 🚦 Throttled (429)" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host " Request $i`: ❌ Failed ($statusCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Small delay between requests
|
|
||||||
Start-Sleep -Milliseconds 100
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n📊 Results:" -ForegroundColor Cyan
|
|
||||||
Write-Host " Successful requests: $successCount" -ForegroundColor Green
|
|
||||||
Write-Host " Throttled requests: $throttleCount" -ForegroundColor Yellow
|
|
||||||
Write-Host " Expected behavior: ~10 success, ~2 throttled" -ForegroundColor Gray
|
|
||||||
|
|
||||||
if ($successCount -ge 8 -and $throttleCount -ge 1) {
|
|
||||||
Write-Host "✅ PASS: Throttle is working correctly" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "⚠️ WARNING: Throttle behavior may need adjustment" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n🎯 Throttle Configuration:" -ForegroundColor Green
|
|
||||||
Write-Host " Register: 10 requests / 5 minutes" -ForegroundColor White
|
|
||||||
Write-Host " Login: 5 requests / 1 minute" -ForegroundColor White
|
|
||||||
Write-Host " Send Code: 1 request / 1 minute" -ForegroundColor White
|
|
||||||
Write-Host " Password Reset: 3 requests / 1 hour" -ForegroundColor White
|
|
||||||
Reference in New Issue
Block a user