diff --git a/README.md b/README.md index 299fc83..e3663f1 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,25 @@ pnpm run dev ### 🧪 快速测试 ```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文件存储功能 - ✅ 邮件测试模式 @@ -323,9 +332,8 @@ pnpm run test:watch # 生成测试覆盖率报告 pnpm run test:cov -# API功能测试 -.\test-api.ps1 # Windows -./test-api.sh # Linux/macOS +# API功能测试(综合测试脚本) +.\test-comprehensive.ps1 ``` ### 📈 测试覆盖率 diff --git a/docs/api/api-documentation.md b/docs/api/api-documentation.md index 0c9f184..26171be 100644 --- a/docs/api/api-documentation.md +++ b/docs/api/api-documentation.md @@ -11,6 +11,8 @@ 3. **验证码有效期**: 所有验证码有效期为5分钟 4. **频率限制**: 验证码发送限制1次/分钟,注册限制10次/5分钟 5. **测试模式**: 开发环境下邮件服务返回206状态码,验证码在响应中返回 +6. **冷却时间自动清除**: 注册、密码重置、验证码登录成功后会自动清除验证码冷却时间,方便后续操作 +7. **邮件模板修复**: 登录验证码现在使用正确的邮件模板,内容为"登录验证码"而非"密码重置" ### 错误处理规范 - **409 Conflict**: 资源冲突(用户名、邮箱已存在) @@ -24,6 +26,7 @@ 2. 邮箱注册流程:先发送验证码 → 检查409冲突 → 使用验证码注册 3. 测试模式下验证码在响应中返回,生产环境需用户查收邮件 4. 实现重试机制处理429频率限制错误 +5. 注册/重置密码成功后,验证码冷却时间会自动清除,可立即发送新验证码 --- @@ -945,6 +948,11 @@ ## 📊 版本更新记录 +### v1.1.2 (2025-12-25) +- **验证码冷却优化**: 注册、密码重置、验证码登录成功后自动清除验证码冷却时间 +- **用户体验提升**: 成功操作后可立即发送新的验证码,无需等待冷却时间 +- **代码健壮性**: 冷却时间清除失败不影响主要业务流程 + ### v1.1.1 (2025-12-25) - **邮箱冲突检测优化**: 发送邮箱验证码前检查邮箱是否已被注册 - **用户体验提升**: 避免向已注册邮箱发送无用验证码 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 39ddfbb..7a4b7fd 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,8 +1,8 @@ openapi: 3.0.3 info: title: Pixel Game Server - Auth API - description: 像素游戏服务器用户认证API接口文档 - 包含验证码登录功能和邮箱冲突检测 - version: 1.1.1 + description: 像素游戏服务器用户认证API接口文档 - 包含验证码登录功能、邮箱冲突检测、验证码冷却优化和邮件模板修复 + version: 1.1.3 contact: name: API Support email: support@example.com @@ -489,7 +489,7 @@ paths: tags: - auth summary: 发送登录验证码 - description: 向用户邮箱或手机发送登录验证码 + description: 向用户邮箱或手机发送登录验证码。邮件内容使用专门的登录验证码模板,标题为"登录验证码",内容说明用于登录验证而非密码重置。 operationId: sendLoginVerificationCode requestBody: required: true diff --git a/src/business/auth/controllers/login.controller.ts b/src/business/auth/controllers/login.controller.ts index a4179d1..37d4a7c 100644 --- a/src/business/auth/controllers/login.controller.ts +++ b/src/business/auth/controllers/login.controller.ts @@ -529,7 +529,7 @@ export class LoginController { */ @ApiOperation({ summary: '发送登录验证码', - description: '向用户邮箱或手机发送登录验证码' + description: '向用户邮箱或手机发送登录验证码。邮件使用专门的登录验证码模板,内容明确标识为登录验证而非密码重置。' }) @ApiBody({ type: SendLoginVerificationCodeDto }) @SwaggerApiResponse({ diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts index b8f810a..0cd3336 100644 --- a/src/core/login_core/login_core.service.spec.ts +++ b/src/core/login_core/login_core.service.spec.ts @@ -51,6 +51,7 @@ describe('LoginCoreService', () => { const mockVerificationService = { generateCode: jest.fn(), verifyCode: jest.fn(), + clearCooldown: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ @@ -146,6 +147,69 @@ describe('LoginCoreService', () => { 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 () => { jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => { throw new BadRequestException('密码长度至少8位'); @@ -231,6 +295,59 @@ describe('LoginCoreService', () => { 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 () => { verificationService.verifyCode.mockResolvedValue(false); @@ -336,82 +453,75 @@ describe('LoginCoreService', () => { ); }); - it('should successfully login with phone verification code', async () => { - const phoneUser = { ...mockUser, phone: '+8613800138000' }; - 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 () => { + it('should successfully login with email verification code and clear cooldown', async () => { + const identifier = 'test@example.com'; const verifiedUser = { ...mockUser, email_verified: true }; usersService.findByEmail.mockResolvedValue(verifiedUser); verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockResolvedValue(undefined); const result = await service.verificationCodeLogin({ - identifier: 'test@example.com', + identifier, verificationCode: '123456' }); expect(result.user).toEqual(verifiedUser); expect(result.isNewUser).toBe(false); - expect(verificationService.verifyCode).toHaveBeenCalledWith( - 'test@example.com', - VerificationCodeType.EMAIL_VERIFICATION, - '123456' + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.EMAIL_VERIFICATION ); }); + 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 () => { const phoneUser = { ...mockUser, phone: '+8613800138000' }; usersService.findAll.mockResolvedValue([phoneUser]); diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index 3d52626..a53f79f 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -239,6 +239,19 @@ export class LoginCoreService { email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证 }); + // 注册成功后清除验证码冷却时间,方便用户后续操作 + if (email) { + try { + await this.verificationService.clearCooldown( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + } catch (error) { + // 清除冷却时间失败不影响注册流程,只记录日志 + console.warn(`清除验证码冷却时间失败: ${email}`, error); + } + } + // 如果提供了邮箱,发送欢迎邮件 if (email) { try { @@ -417,9 +430,22 @@ export class LoginCoreService { const passwordHash = await this.hashPassword(newPassword); // 更新密码 - return await this.usersService.update(user.id, { + const updatedUser = await this.usersService.update(user.id, { 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; } - // 5. 验证成功,返回用户信息 + // 5. 验证成功后清除验证码冷却时间 + try { + await this.verificationService.clearCooldown(identifier, verificationType); + } catch (error) { + // 清除冷却时间失败不影响登录流程,只记录日志 + console.warn(`清除验证码冷却时间失败: ${identifier}`, error); + } + + // 6. 验证成功,返回用户信息 return { user, isNewUser: false diff --git a/src/core/utils/email/email.service.spec.ts b/src/core/utils/email/email.service.spec.ts index 1cbbf59..1208760 100644 --- a/src/core/utils/email/email.service.spec.ts +++ b/src/core/utils/email/email.service.spec.ts @@ -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" '); + + 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 () => { const options: VerificationEmailOptions = { email: 'test@example.com', @@ -323,6 +347,26 @@ describe('EmailService', () => { 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 () => { mockTransporter.sendMail.mockImplementation((mailOptions: any) => { expect(mailOptions.html).toContain('测试用户'); diff --git a/src/core/utils/email/email.service.ts b/src/core/utils/email/email.service.ts index ac967ea..ee34db7 100644 --- a/src/core/utils/email/email.service.ts +++ b/src/core/utils/email/email.service.ts @@ -172,7 +172,7 @@ export class EmailService { template = this.getPasswordResetTemplate(code, nickname); } else if (purpose === 'login_verification') { subject = '【Whale Town】登录验证码'; - template = this.getPasswordResetTemplate(code, nickname); // 复用密码重置模板 + template = this.getLoginVerificationTemplate(code, nickname); } else { subject = '【Whale Town】验证码'; template = this.getEmailVerificationTemplate(code, nickname); diff --git a/src/core/utils/verification/verification.service.spec.ts b/src/core/utils/verification/verification.service.spec.ts index 72f218b..065843c 100644 --- a/src/core/utils/verification/verification.service.spec.ts +++ b/src/core/utils/verification/verification.service.spec.ts @@ -587,4 +587,69 @@ describe('VerificationService', () => { 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); + }); + }); }); \ No newline at end of file diff --git a/src/core/utils/verification/verification.service.ts b/src/core/utils/verification/verification.service.ts index 51207ab..ab07898 100644 --- a/src/core/utils/verification/verification.service.ts +++ b/src/core/utils/verification/verification.service.ts @@ -294,6 +294,18 @@ export class VerificationService { return `verification_hourly:${type}:${identifier}:${date}:${hour}`; } + /** + * 清除验证码冷却时间 + * + * @param identifier 标识符 + * @param type 验证码类型 + */ + async clearCooldown(identifier: string, type: VerificationCodeType): Promise { + const cooldownKey = this.buildCooldownKey(identifier, type); + await this.redis.del(cooldownKey); + this.logger.log(`验证码冷却时间已清除: ${identifier} (${type})`); + } + /** * 清理过期的验证码(可选的定时任务) */ diff --git a/test-api.ps1 b/test-api.ps1 deleted file mode 100644 index cd432c9..0000000 --- a/test-api.ps1 +++ /dev/null @@ -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 \ No newline at end of file diff --git a/test-api.sh b/test-api.sh deleted file mode 100644 index a4fb3c0..0000000 --- a/test-api.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/test-comprehensive.ps1 b/test-comprehensive.ps1 new file mode 100644 index 0000000..41d36f6 --- /dev/null +++ b/test-comprehensive.ps1 @@ -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 \ No newline at end of file diff --git a/test-register-fix.ps1 b/test-register-fix.ps1 deleted file mode 100644 index 0bf5f9e..0000000 --- a/test-register-fix.ps1 +++ /dev/null @@ -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 \ No newline at end of file diff --git a/test-throttle.ps1 b/test-throttle.ps1 deleted file mode 100644 index fafe0b5..0000000 --- a/test-throttle.ps1 +++ /dev/null @@ -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 \ No newline at end of file