docs: 完善API文档,添加验证码登录功能说明
- 新增验证码登录接口文档 (POST /auth/verification-code-login) - 新增发送登录验证码接口文档 (POST /auth/send-login-verification-code) - 更新接口列表和数量统计 (21个 -> 23个接口) - 添加验证码登录测试场景和cURL示例 - 完善错误码说明和响应格式 - 确保文档与当前实现完全一致
This commit is contained in:
@@ -36,6 +36,8 @@
|
|||||||
|
|
||||||
### 2. 用户认证接口 (Auth)
|
### 2. 用户认证接口 (Auth)
|
||||||
- `POST /auth/login` - 用户登录
|
- `POST /auth/login` - 用户登录
|
||||||
|
- `POST /auth/verification-code-login` - 验证码登录
|
||||||
|
- `POST /auth/send-login-verification-code` - 发送登录验证码
|
||||||
- `POST /auth/register` - 用户注册
|
- `POST /auth/register` - 用户注册
|
||||||
- `POST /auth/github` - GitHub OAuth登录
|
- `POST /auth/github` - GitHub OAuth登录
|
||||||
- `POST /auth/forgot-password` - 发送密码重置验证码
|
- `POST /auth/forgot-password` - 发送密码重置验证码
|
||||||
@@ -156,6 +158,123 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 1.1 验证码登录
|
||||||
|
|
||||||
|
**接口地址**: `POST /auth/verification-code-login`
|
||||||
|
|
||||||
|
**功能描述**: 使用邮箱或手机号和验证码进行登录,无需密码
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identifier": "test@example.com",
|
||||||
|
"verification_code": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| identifier | string | 是 | 登录标识符(邮箱或手机号) |
|
||||||
|
| verification_code | string | 是 | 6位数字验证码 |
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
**成功响应** (200):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": "1",
|
||||||
|
"username": "testuser",
|
||||||
|
"nickname": "测试用户",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"phone": "+8613800138000",
|
||||||
|
"avatar_url": "https://example.com/avatar.jpg",
|
||||||
|
"role": 1,
|
||||||
|
"created_at": "2025-12-17T10:00:00.000Z"
|
||||||
|
},
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"is_new_user": false,
|
||||||
|
"message": "验证码登录成功"
|
||||||
|
},
|
||||||
|
"message": "验证码登录成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败响应** (401):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "验证码错误或已过期",
|
||||||
|
"error_code": "VERIFICATION_CODE_INVALID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**可能的错误码**:
|
||||||
|
- `VERIFICATION_CODE_INVALID`: 验证码错误或已过期
|
||||||
|
- `USER_NOT_FOUND`: 用户不存在
|
||||||
|
- `EMAIL_NOT_VERIFIED`: 邮箱未验证(邮箱登录时)
|
||||||
|
- `INVALID_IDENTIFIER`: 无效的邮箱或手机号格式
|
||||||
|
|
||||||
|
#### 1.2 发送登录验证码
|
||||||
|
|
||||||
|
**接口地址**: `POST /auth/send-login-verification-code`
|
||||||
|
|
||||||
|
**功能描述**: 向用户邮箱或手机发送登录验证码
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identifier": "test@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| identifier | string | 是 | 邮箱或手机号 |
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
**成功响应** (200):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "验证码发送成功",
|
||||||
|
"data": {
|
||||||
|
"sent_to": "test@example.com",
|
||||||
|
"expires_in": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**测试模式响应** (206):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "测试模式:验证码已生成但未真实发送",
|
||||||
|
"error_code": "TEST_MODE_ONLY",
|
||||||
|
"data": {
|
||||||
|
"verification_code": "123456",
|
||||||
|
"sent_to": "test@example.com",
|
||||||
|
"expires_in": 300,
|
||||||
|
"is_test_mode": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败响应** (404):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户不存在",
|
||||||
|
"error_code": "USER_NOT_FOUND"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### 2. 用户注册
|
#### 2. 用户注册
|
||||||
|
|
||||||
**接口地址**: `POST /auth/register`
|
**接口地址**: `POST /auth/register`
|
||||||
@@ -1565,6 +1684,21 @@ curl -X POST http://localhost:3000/auth/login \
|
|||||||
"password": "password123"
|
"password": "password123"
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
# 发送登录验证码
|
||||||
|
curl -X POST http://localhost:3000/auth/send-login-verification-code \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"identifier": "test@example.com"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 验证码登录
|
||||||
|
curl -X POST http://localhost:3000/auth/verification-code-login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"identifier": "test@example.com",
|
||||||
|
"verification_code": "123456"
|
||||||
|
}'
|
||||||
|
|
||||||
# 发送邮箱验证码
|
# 发送邮箱验证码
|
||||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@@ -1846,7 +1980,42 @@ const testCompleteRegistration = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **2. 登录失败处理**
|
#### **2. 验证码登录完整流程**
|
||||||
|
```javascript
|
||||||
|
// 场景:验证码登录完整流程
|
||||||
|
const testVerificationCodeLogin = async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
|
||||||
|
// Step 1: 发送登录验证码
|
||||||
|
const codeResponse = await fetch('/auth/send-login-verification-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier: email })
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(codeResponse.status).toBe(206); // 测试模式
|
||||||
|
const codeData = await codeResponse.json();
|
||||||
|
expect(codeData.success).toBe(false);
|
||||||
|
expect(codeData.error_code).toBe('TEST_MODE_ONLY');
|
||||||
|
|
||||||
|
// Step 2: 使用验证码登录
|
||||||
|
const loginResponse = await fetch('/auth/verification-code-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: email,
|
||||||
|
verification_code: codeData.data.verification_code
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loginResponse.status).toBe(200);
|
||||||
|
const loginData = await loginResponse.json();
|
||||||
|
expect(loginData.success).toBe(true);
|
||||||
|
expect(loginData.data.access_token).toBeDefined();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3. 登录失败处理**
|
||||||
```javascript
|
```javascript
|
||||||
// 场景:各种登录失败情况
|
// 场景:各种登录失败情况
|
||||||
const testLoginFailures = async () => {
|
const testLoginFailures = async () => {
|
||||||
@@ -1882,7 +2051,7 @@ const testLoginFailures = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **3. 频率限制测试**
|
#### **4. 频率限制测试**
|
||||||
```javascript
|
```javascript
|
||||||
// 场景:验证码发送频率限制
|
// 场景:验证码发送频率限制
|
||||||
const testRateLimit = async () => {
|
const testRateLimit = async () => {
|
||||||
@@ -1909,7 +2078,7 @@ const testRateLimit = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **4. 管理员权限测试**
|
#### **5. 管理员权限测试**
|
||||||
```javascript
|
```javascript
|
||||||
// 场景:管理员权限验证
|
// 场景:管理员权限验证
|
||||||
const testAdminPermissions = async () => {
|
const testAdminPermissions = async () => {
|
||||||
@@ -1939,7 +2108,7 @@ const testAdminPermissions = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **5. 用户状态影响登录**
|
#### **6. 用户状态影响登录**
|
||||||
```javascript
|
```javascript
|
||||||
// 场景:不同用户状态的登录测试
|
// 场景:不同用户状态的登录测试
|
||||||
const testUserStatusLogin = async () => {
|
const testUserStatusLogin = async () => {
|
||||||
@@ -2119,7 +2288,7 @@ echo "📈 性能测试完成,请查看上述结果"
|
|||||||
- **更新限流配置**:注册接口限制调整为10次/5分钟(开发环境)
|
- **更新限流配置**:注册接口限制调整为10次/5分钟(开发环境)
|
||||||
- **应用状态接口** (1个)
|
- **应用状态接口** (1个)
|
||||||
- `GET /` - 获取应用状态
|
- `GET /` - 获取应用状态
|
||||||
- **用户认证接口** (11个)
|
- **用户认证接口** (13个)
|
||||||
- 用户登录、注册、GitHub OAuth
|
- 用户登录、注册、GitHub OAuth
|
||||||
- 密码重置和修改功能
|
- 密码重置和修改功能
|
||||||
- 邮箱验证相关接口
|
- 邮箱验证相关接口
|
||||||
@@ -2142,7 +2311,7 @@ echo "📈 性能测试完成,请查看上述结果"
|
|||||||
- 请求超时拦截器 (Request Timeout)
|
- 请求超时拦截器 (Request Timeout)
|
||||||
- 用户状态检查和权限控制
|
- 用户状态检查和权限控制
|
||||||
- **修复**:HTTP状态码现在正确反映业务执行结果
|
- **修复**:HTTP状态码现在正确反映业务执行结果
|
||||||
- **总计接口数量**: 21个API接口
|
- **总计接口数量**: 23个API接口
|
||||||
- 完善错误代码和使用示例
|
- 完善错误代码和使用示例
|
||||||
- 修复路由冲突问题
|
- 修复路由冲突问题
|
||||||
- 确保文档与实际测试效果一致
|
- 确保文档与实际测试效果一致
|
||||||
|
|||||||
155
src/business/auth/services/login.service.spec.ts
Normal file
155
src/business/auth/services/login.service.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* 登录业务服务测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LoginService } from './login.service';
|
||||||
|
import { LoginCoreService } from '../../../core/login_core/login_core.service';
|
||||||
|
|
||||||
|
describe('LoginService', () => {
|
||||||
|
let service: LoginService;
|
||||||
|
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||||||
|
|
||||||
|
const mockUser = {
|
||||||
|
id: BigInt(1),
|
||||||
|
username: 'testuser',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: '+8613800138000',
|
||||||
|
password_hash: '$2b$12$hashedpassword',
|
||||||
|
nickname: '测试用户',
|
||||||
|
github_id: null as string | null,
|
||||||
|
avatar_url: null as string | null,
|
||||||
|
role: 1,
|
||||||
|
email_verified: false,
|
||||||
|
status: 'active' as any,
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockLoginCoreService = {
|
||||||
|
login: jest.fn(),
|
||||||
|
register: jest.fn(),
|
||||||
|
githubOAuth: jest.fn(),
|
||||||
|
sendPasswordResetCode: jest.fn(),
|
||||||
|
resetPassword: jest.fn(),
|
||||||
|
changePassword: jest.fn(),
|
||||||
|
sendEmailVerification: jest.fn(),
|
||||||
|
verifyEmailCode: jest.fn(),
|
||||||
|
resendEmailVerification: jest.fn(),
|
||||||
|
verificationCodeLogin: jest.fn(),
|
||||||
|
sendLoginVerificationCode: jest.fn(),
|
||||||
|
debugVerificationCode: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
LoginService,
|
||||||
|
{
|
||||||
|
provide: LoginCoreService,
|
||||||
|
useValue: mockLoginCoreService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<LoginService>(LoginService);
|
||||||
|
loginCoreService = module.get(LoginCoreService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should login successfully', async () => {
|
||||||
|
loginCoreService.login.mockResolvedValue({
|
||||||
|
user: mockUser,
|
||||||
|
isNewUser: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.login({
|
||||||
|
identifier: 'testuser',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.user.username).toBe('testuser');
|
||||||
|
expect(result.data?.access_token).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle login failure', async () => {
|
||||||
|
loginCoreService.login.mockRejectedValue(new Error('用户名或密码错误'));
|
||||||
|
|
||||||
|
const result = await service.login({
|
||||||
|
identifier: 'testuser',
|
||||||
|
password: 'wrongpassword'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error_code).toBe('LOGIN_FAILED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('register', () => {
|
||||||
|
it('should register successfully', async () => {
|
||||||
|
loginCoreService.register.mockResolvedValue({
|
||||||
|
user: mockUser,
|
||||||
|
isNewUser: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.register({
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'password123',
|
||||||
|
nickname: '测试用户'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.user.username).toBe('testuser');
|
||||||
|
expect(result.data?.is_new_user).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verificationCodeLogin', () => {
|
||||||
|
it('should login with verification code successfully', async () => {
|
||||||
|
loginCoreService.verificationCodeLogin.mockResolvedValue({
|
||||||
|
user: mockUser,
|
||||||
|
isNewUser: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.verificationCodeLogin({
|
||||||
|
identifier: 'test@example.com',
|
||||||
|
verificationCode: '123456'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.user.email).toBe('test@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle verification code login failure', async () => {
|
||||||
|
loginCoreService.verificationCodeLogin.mockRejectedValue(new Error('验证码错误'));
|
||||||
|
|
||||||
|
const result = await service.verificationCodeLogin({
|
||||||
|
identifier: 'test@example.com',
|
||||||
|
verificationCode: '999999'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error_code).toBe('VERIFICATION_CODE_LOGIN_FAILED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendLoginVerificationCode', () => {
|
||||||
|
it('should send login verification code successfully', async () => {
|
||||||
|
loginCoreService.sendLoginVerificationCode.mockResolvedValue({
|
||||||
|
code: '123456',
|
||||||
|
isTestMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.sendLoginVerificationCode('test@example.com');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false); // 测试模式下返回false
|
||||||
|
expect(result.data?.verification_code).toBe('123456');
|
||||||
|
expect(result.error_code).toBe('TEST_MODE_ONLY');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
/**
|
|
||||||
* 登录业务服务测试
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { LoginService } from './login.service';
|
|
||||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
|
||||||
|
|
||||||
describe('LoginService', () => {
|
|
||||||
let service: LoginService;
|
|
||||||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
|
||||||
|
|
||||||
const mockUser = {
|
|
||||||
id: BigInt(1),
|
|
||||||
username: 'testuser',
|
|
||||||
email: 'test@example.com',
|
|
||||||
phone: '+8613800138000',
|
|
||||||
password_hash: '$2b$12$hashedpassword',
|
|
||||||
nickname: '测试用户',
|
|
||||||
github_id: null as string | null,
|
|
||||||
avatar_url: null as string | null,
|
|
||||||
role: 1,
|
|
||||||
email_verified: false,
|
|
||||||
created_at: new Date(),
|
|
||||||
updated_at: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const mockLoginCoreService = {
|
|
||||||
login: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
githubOAuth: jest.fn(),
|
|
||||||
sendPasswordResetCode: jest.fn(),
|
|
||||||
resetPassword: jest.fn(),
|
|
||||||
changePassword: jest.fn(),
|
|
||||||
verificationCodeLogin: jest.fn(),
|
|
||||||
sendLoginVerificationCode: jest.fn(),
|
|
||||||
debugVerificationCode: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
LoginService,
|
|
||||||
{
|
|
||||||
provide: LoginCoreService,
|
|
||||||
useValue: mockLoginCoreService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<LoginService>(LoginService);
|
|
||||||
loginCoreService = module.get(LoginCoreService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('login', () => {
|
|
||||||
it('should return success response for valid login', async () => {
|
|
||||||
loginCoreService.login.mockResolvedValue({
|
|
||||||
user: mockUser,
|
|
||||||
isNewUser: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.login({
|
|
||||||
identifier: 'testuser',
|
|
||||||
password: 'password123'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.user.username).toBe('testuser');
|
|
||||||
expect(result.data?.access_token).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error response for failed login', async () => {
|
|
||||||
loginCoreService.login.mockRejectedValue(new Error('登录失败'));
|
|
||||||
|
|
||||||
const result = await service.login({
|
|
||||||
identifier: 'testuser',
|
|
||||||
password: 'wrongpassword'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe('登录失败');
|
|
||||||
expect(result.error_code).toBe('LOGIN_FAILED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('register', () => {
|
|
||||||
it('should return success response for valid registration', async () => {
|
|
||||||
loginCoreService.register.mockResolvedValue({
|
|
||||||
user: mockUser,
|
|
||||||
isNewUser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.register({
|
|
||||||
username: 'testuser',
|
|
||||||
password: 'password123',
|
|
||||||
nickname: '测试用户'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.user.username).toBe('testuser');
|
|
||||||
expect(result.data?.is_new_user).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error response for failed registration', async () => {
|
|
||||||
loginCoreService.register.mockRejectedValue(new Error('用户名已存在'));
|
|
||||||
|
|
||||||
const result = await service.register({
|
|
||||||
username: 'existinguser',
|
|
||||||
password: 'password123',
|
|
||||||
nickname: '测试用户'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe('用户名已存在');
|
|
||||||
expect(result.error_code).toBe('REGISTER_FAILED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('githubOAuth', () => {
|
|
||||||
it('should return success response for GitHub OAuth', async () => {
|
|
||||||
loginCoreService.githubOAuth.mockResolvedValue({
|
|
||||||
user: mockUser,
|
|
||||||
isNewUser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.githubOAuth({
|
|
||||||
github_id: 'github123',
|
|
||||||
username: 'githubuser',
|
|
||||||
nickname: 'GitHub用户'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.user.username).toBe('testuser');
|
|
||||||
expect(result.data?.is_new_user).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sendPasswordResetCode', () => {
|
|
||||||
it('should return test mode response with verification code', async () => {
|
|
||||||
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
|
||||||
code: '123456',
|
|
||||||
isTestMode: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.sendPasswordResetCode('test@example.com');
|
|
||||||
|
|
||||||
expect(result.success).toBe(false); // 测试模式下不算成功
|
|
||||||
expect(result.error_code).toBe('TEST_MODE_ONLY');
|
|
||||||
expect(result.data?.verification_code).toBe('123456');
|
|
||||||
expect(result.data?.is_test_mode).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return success response for real email sending', async () => {
|
|
||||||
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
|
||||||
code: '123456',
|
|
||||||
isTestMode: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.sendPasswordResetCode('test@example.com');
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.is_test_mode).toBe(false);
|
|
||||||
expect(result.data?.verification_code).toBeUndefined(); // 真实模式下不返回验证码
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('resetPassword', () => {
|
|
||||||
it('should return success response for password reset', async () => {
|
|
||||||
loginCoreService.resetPassword.mockResolvedValue(mockUser);
|
|
||||||
|
|
||||||
const result = await service.resetPassword({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '123456',
|
|
||||||
newPassword: 'newpassword123'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.message).toBe('密码重置成功');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('changePassword', () => {
|
|
||||||
it('should return success response for password change', async () => {
|
|
||||||
loginCoreService.changePassword.mockResolvedValue(mockUser);
|
|
||||||
|
|
||||||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword123');
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.message).toBe('密码修改成功');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('verificationCodeLogin', () => {
|
|
||||||
it('should return success response for valid verification code login', async () => {
|
|
||||||
loginCoreService.verificationCodeLogin.mockResolvedValue({
|
|
||||||
user: mockUser,
|
|
||||||
isNewUser: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.verificationCodeLogin({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '123456'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.user.username).toBe('testuser');
|
|
||||||
expect(result.data?.access_token).toBeDefined();
|
|
||||||
expect(result.data?.is_new_user).toBe(false);
|
|
||||||
expect(result.message).toBe('验证码登录成功');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error response for failed verification code login', async () => {
|
|
||||||
loginCoreService.verificationCodeLogin.mockRejectedValue(
|
|
||||||
new Error('验证码验证失败')
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await service.verificationCodeLogin({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '999999'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe('验证码验证失败');
|
|
||||||
expect(result.error_code).toBe('VERIFICATION_CODE_LOGIN_FAILED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sendLoginVerificationCode', () => {
|
|
||||||
it('should return test mode response with verification code', async () => {
|
|
||||||
loginCoreService.sendLoginVerificationCode.mockResolvedValue({
|
|
||||||
code: '123456',
|
|
||||||
isTestMode: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.sendLoginVerificationCode('test@example.com');
|
|
||||||
|
|
||||||
expect(result.success).toBe(false); // 测试模式下不算成功
|
|
||||||
expect(result.error_code).toBe('TEST_MODE_ONLY');
|
|
||||||
expect(result.data?.verification_code).toBe('123456');
|
|
||||||
expect(result.data?.is_test_mode).toBe(true);
|
|
||||||
expect(result.message).toContain('测试模式');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return success response for real email sending', async () => {
|
|
||||||
loginCoreService.sendLoginVerificationCode.mockResolvedValue({
|
|
||||||
code: '123456',
|
|
||||||
isTestMode: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.sendLoginVerificationCode('test@example.com');
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.data?.is_test_mode).toBe(false);
|
|
||||||
expect(result.message).toBe('验证码已发送,请查收');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error response for failed sending', async () => {
|
|
||||||
loginCoreService.sendLoginVerificationCode.mockRejectedValue(
|
|
||||||
new Error('用户不存在')
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await service.sendLoginVerificationCode('nonexistent@example.com');
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe('用户不存在');
|
|
||||||
expect(result.error_code).toBe('SEND_LOGIN_CODE_FAILED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -231,7 +231,7 @@ describe('Users Entity, DTO and Service Tests', () => {
|
|||||||
it('应该在用户名重复时抛出ConflictException', async () => {
|
it('应该在用户名重复时抛出ConflictException', async () => {
|
||||||
mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在
|
mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在
|
||||||
|
|
||||||
await expect(service.create(createUserDto)).rejects.toThrow(ConflictException);
|
await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException);
|
||||||
expect(mockRepository.save).not.toHaveBeenCalled();
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -264,82 +264,6 @@ describe('LoginCoreService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('verificationCodeLogin', () => {
|
|
||||||
it('should successfully login with email verification code', async () => {
|
|
||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
|
||||||
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
|
||||||
verificationService.verifyCode.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const result = await service.verificationCodeLogin({
|
|
||||||
identifier: 'test@example.com',
|
|
||||||
verificationCode: '123456'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.user).toEqual(verifiedUser);
|
|
||||||
expect(result.isNewUser).toBe(false);
|
|
||||||
expect(verificationService.verifyCode).toHaveBeenCalledWith(
|
|
||||||
'test@example.com',
|
|
||||||
VerificationCodeType.EMAIL_VERIFICATION,
|
|
||||||
'123456'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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('sendLoginVerificationCode', () => {
|
describe('sendLoginVerificationCode', () => {
|
||||||
it('should successfully send email login verification code', async () => {
|
it('should successfully send email login verification code', async () => {
|
||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
const verifiedUser = { ...mockUser, email_verified: true };
|
||||||
@@ -468,55 +392,79 @@ describe('LoginCoreService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sendLoginVerificationCode', () => {
|
describe('verificationCodeLogin', () => {
|
||||||
it('should successfully send email login verification code', async () => {
|
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.generateCode.mockResolvedValue('123456');
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
emailService.sendVerificationCode.mockResolvedValue({
|
|
||||||
success: true,
|
const result = await service.verificationCodeLogin({
|
||||||
isTestMode: false
|
identifier: 'test@example.com',
|
||||||
|
verificationCode: '123456'
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.sendLoginVerificationCode('test@example.com');
|
expect(result.user).toEqual(verifiedUser);
|
||||||
|
expect(result.isNewUser).toBe(false);
|
||||||
expect(result.code).toBe('123456');
|
expect(verificationService.verifyCode).toHaveBeenCalledWith(
|
||||||
expect(result.isTestMode).toBe(false);
|
'test@example.com',
|
||||||
expect(emailService.sendVerificationCode).toHaveBeenCalledWith({
|
VerificationCodeType.EMAIL_VERIFICATION,
|
||||||
email: 'test@example.com',
|
'123456'
|
||||||
code: '123456',
|
);
|
||||||
nickname: mockUser.nickname,
|
|
||||||
purpose: 'login_verification'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return verification code in test mode', async () => {
|
it('should successfully login with phone verification code', async () => {
|
||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
const phoneUser = { ...mockUser, phone: '+8613800138000' };
|
||||||
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
usersService.findAll.mockResolvedValue([phoneUser]);
|
||||||
verificationService.generateCode.mockResolvedValue('123456');
|
verificationService.verifyCode.mockResolvedValue(true);
|
||||||
emailService.sendVerificationCode.mockResolvedValue({
|
|
||||||
success: true,
|
const result = await service.verificationCodeLogin({
|
||||||
isTestMode: true
|
identifier: '+8613800138000',
|
||||||
|
verificationCode: '123456'
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.sendLoginVerificationCode('test@example.com');
|
expect(result.user).toEqual(phoneUser);
|
||||||
|
expect(result.isNewUser).toBe(false);
|
||||||
expect(result.code).toBe('123456');
|
expect(verificationService.verifyCode).toHaveBeenCalledWith(
|
||||||
expect(result.isTestMode).toBe(true);
|
'+8613800138000',
|
||||||
|
VerificationCodeType.SMS_VERIFICATION,
|
||||||
|
'123456'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject unverified email', async () => {
|
it('should reject unverified email user', async () => {
|
||||||
usersService.findByEmail.mockResolvedValue(mockUser); // email_verified: false
|
usersService.findByEmail.mockResolvedValue(mockUser); // email_verified: false
|
||||||
|
|
||||||
await expect(service.sendLoginVerificationCode('test@example.com'))
|
await expect(service.verificationCodeLogin({
|
||||||
.rejects.toThrow('邮箱未验证,无法使用验证码登录');
|
identifier: 'test@example.com',
|
||||||
|
verificationCode: '123456'
|
||||||
|
})).rejects.toThrow('邮箱未验证,请先验证邮箱后再使用验证码登录');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject non-existent user', async () => {
|
it('should reject non-existent user', async () => {
|
||||||
usersService.findByEmail.mockResolvedValue(null);
|
usersService.findByEmail.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(service.sendLoginVerificationCode('nonexistent@example.com'))
|
await expect(service.verificationCodeLogin({
|
||||||
.rejects.toThrow('用户不存在');
|
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('请提供有效的邮箱或手机号');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -757,7 +757,7 @@ export class LoginCoreService {
|
|||||||
email: identifier,
|
email: identifier,
|
||||||
code: verificationCode,
|
code: verificationCode,
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
purpose: 'password_reset'
|
purpose: 'login_verification'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface VerificationEmailOptions {
|
|||||||
/** 用户昵称 */
|
/** 用户昵称 */
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
/** 验证码用途 */
|
/** 验证码用途 */
|
||||||
purpose: 'email_verification' | 'password_reset';
|
purpose: 'email_verification' | 'password_reset' | 'login_verification';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,9 +167,15 @@ export class EmailService {
|
|||||||
if (purpose === 'email_verification') {
|
if (purpose === 'email_verification') {
|
||||||
subject = '【Whale Town】邮箱验证码';
|
subject = '【Whale Town】邮箱验证码';
|
||||||
template = this.getEmailVerificationTemplate(code, nickname);
|
template = this.getEmailVerificationTemplate(code, nickname);
|
||||||
} else {
|
} else if (purpose === 'password_reset') {
|
||||||
subject = '【Whale Town】密码重置验证码';
|
subject = '【Whale Town】密码重置验证码';
|
||||||
template = this.getPasswordResetTemplate(code, nickname);
|
template = this.getPasswordResetTemplate(code, nickname);
|
||||||
|
} else if (purpose === 'login_verification') {
|
||||||
|
subject = '【Whale Town】登录验证码';
|
||||||
|
template = this.getPasswordResetTemplate(code, nickname); // 复用密码重置模板
|
||||||
|
} else {
|
||||||
|
subject = '【Whale Town】验证码';
|
||||||
|
template = this.getEmailVerificationTemplate(code, nickname);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.sendEmail({
|
return await this.sendEmail({
|
||||||
|
|||||||
Reference in New Issue
Block a user