add test case
This commit is contained in:
parent
d2692e6a11
commit
769973517b
File diff suppressed because it is too large
Load Diff
282
test/controllers/README.md
Normal file
282
test/controllers/README.md
Normal file
@ -0,0 +1,282 @@
|
||||
# Controller Tests
|
||||
|
||||
这个目录包含了NestJS应用程序中所有controller的完整测试用例。每个controller都有独立的测试文件,覆盖了所有的API接口和各种测试场景。
|
||||
|
||||
## 📁 测试文件结构
|
||||
|
||||
```
|
||||
test/controllers/
|
||||
├── README.md # 本文件
|
||||
├── category.controller.test.ts # 分类查询API测试
|
||||
├── tag.controller.test.ts # 标签查询API测试
|
||||
├── post.controller.test.ts # 文章操作API测试
|
||||
├── comment.controller.test.ts # 评论操作API测试
|
||||
├── user.controller.test.ts # 用户查询API测试
|
||||
├── account.controller.test.ts # 账户操作API测试
|
||||
├── role.controller.test.ts # 角色查询API测试
|
||||
└── manager/ # 管理后台API测试
|
||||
├── category.controller.test.ts # 分类管理API测试
|
||||
├── tag.controller.test.ts # 标签管理API测试
|
||||
├── post.controller.test.ts # 文章管理API测试
|
||||
├── comment.controller.test.ts # 评论管理API测试
|
||||
├── user.controller.test.ts # 用户管理API测试
|
||||
├── role.controller.test.ts # 角色管理API测试
|
||||
└── permission.controller.test.ts # 权限管理API测试
|
||||
```
|
||||
|
||||
## 🧪 测试覆盖范围
|
||||
|
||||
每个测试文件都包含以下类型的测试用例:
|
||||
|
||||
### ✅ 成功场景测试
|
||||
- 正常的API调用和响应
|
||||
- 有效的参数和数据
|
||||
- 正确的权限验证
|
||||
|
||||
### ❌ 失败场景测试
|
||||
- 参数验证失败
|
||||
- 权限验证失败
|
||||
- 数据不存在
|
||||
- 业务逻辑错误
|
||||
|
||||
### 🔍 边界值测试
|
||||
- 最大/最小值测试
|
||||
- 空值和null值测试
|
||||
- 特殊字符测试
|
||||
- 长度限制测试
|
||||
|
||||
### 🔐 权限测试
|
||||
- 未认证访问
|
||||
- 权限不足
|
||||
- Token验证
|
||||
|
||||
## 🚀 运行测试
|
||||
|
||||
### 运行所有controller测试
|
||||
```bash
|
||||
npm run test:controllers
|
||||
```
|
||||
|
||||
### 运行特定的测试文件
|
||||
```bash
|
||||
# 运行分类controller测试
|
||||
npm run test:controllers category
|
||||
|
||||
# 运行用户相关测试
|
||||
npm run test:controllers user account
|
||||
|
||||
# 运行管理后台测试
|
||||
npm run test:controllers manager
|
||||
```
|
||||
|
||||
### 运行测试组
|
||||
```bash
|
||||
# 运行所有前台API测试
|
||||
npm run test:controllers app
|
||||
|
||||
# 运行所有管理后台API测试
|
||||
npm run test:controllers manager
|
||||
|
||||
# 运行所有内容相关测试
|
||||
npm run test:controllers content
|
||||
|
||||
# 运行所有用户认证相关测试
|
||||
npm run test:controllers user-auth
|
||||
|
||||
# 运行所有权限相关测试
|
||||
npm run test:controllers rbac
|
||||
```
|
||||
|
||||
### 使用Jest直接运行
|
||||
```bash
|
||||
# 运行单个测试文件
|
||||
npx jest test/controllers/category.controller.test.ts
|
||||
|
||||
# 运行所有controller测试
|
||||
npx jest test/controllers/
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npx jest test/controllers/ --coverage
|
||||
|
||||
# 运行测试并监听文件变化
|
||||
npx jest test/controllers/ --watch
|
||||
```
|
||||
|
||||
## 📊 测试报告
|
||||
|
||||
测试运行后会生成以下报告:
|
||||
|
||||
### 控制台输出
|
||||
- 测试执行摘要
|
||||
- 失败测试详情
|
||||
- 性能统计
|
||||
- 覆盖率摘要
|
||||
|
||||
### HTML报告
|
||||
- 位置:`coverage/controllers/test-report.html`
|
||||
- 包含详细的测试结果和覆盖率信息
|
||||
|
||||
### JSON报告
|
||||
- 位置:`coverage/controllers/test-results.json`
|
||||
- 机器可读的测试结果数据
|
||||
|
||||
### 覆盖率报告
|
||||
- 位置:`coverage/controllers/lcov-report/index.html`
|
||||
- 详细的代码覆盖率分析
|
||||
|
||||
## 🛠️ 测试配置
|
||||
|
||||
### Jest配置
|
||||
测试使用专门的Jest配置文件:`test/jest.controller.config.js`
|
||||
|
||||
### 环境变量
|
||||
测试运行时会使用以下环境变量:
|
||||
```bash
|
||||
NODE_ENV=test
|
||||
APP_PORT=3001
|
||||
TEST_DB_HOST=localhost
|
||||
TEST_DB_PORT=3306
|
||||
TEST_DB_DATABASE=nestapp_test
|
||||
TEST_DB_USERNAME=root
|
||||
TEST_DB_PASSWORD=
|
||||
```
|
||||
|
||||
### 测试数据库
|
||||
- 测试使用独立的测试数据库
|
||||
- 每个测试都会创建和清理自己的测试数据
|
||||
- 不会影响开发或生产数据
|
||||
|
||||
## 📝 编写新的测试
|
||||
|
||||
### 测试文件模板
|
||||
```typescript
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/your-module';
|
||||
|
||||
describe('YourController', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let testData: any[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试数据
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
// 清理测试数据
|
||||
}
|
||||
|
||||
describe('GET /endpoint', () => {
|
||||
it('should return success response', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/endpoint`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
// 添加更多断言
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 测试数据生成
|
||||
使用 `test/helpers/test-data-generator.ts` 中的工具函数:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
generateTestUser,
|
||||
generateTestCategory,
|
||||
TestDataManager
|
||||
} from '../helpers/test-data-generator';
|
||||
|
||||
const dataManager = new TestDataManager();
|
||||
|
||||
// 生成测试用户
|
||||
const testUser = generateTestUser('mytest');
|
||||
|
||||
// 生成测试分类
|
||||
const testCategory = generateTestCategory('MyTestCategory');
|
||||
|
||||
// 添加清理任务
|
||||
dataManager.getCleaner().addCleanupTask(async () => {
|
||||
await userRepository.remove(testUser);
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **数据库连接失败**
|
||||
- 检查测试数据库是否存在
|
||||
- 确认数据库连接配置正确
|
||||
|
||||
2. **测试超时**
|
||||
- 增加Jest超时时间
|
||||
- 检查是否有未关闭的数据库连接
|
||||
|
||||
3. **权限测试失败**
|
||||
- 确认测试用户有正确的权限
|
||||
- 检查认证token是否有效
|
||||
|
||||
4. **测试数据冲突**
|
||||
- 确保每个测试使用独立的测试数据
|
||||
- 检查测试数据清理是否完整
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. **启用详细日志**
|
||||
```bash
|
||||
DISABLE_TEST_LOGS=false npm run test:controllers
|
||||
```
|
||||
|
||||
2. **运行单个测试**
|
||||
```bash
|
||||
npx jest test/controllers/category.controller.test.ts --verbose
|
||||
```
|
||||
|
||||
3. **使用调试器**
|
||||
```bash
|
||||
node --inspect-brk node_modules/.bin/jest test/controllers/category.controller.test.ts --runInBand
|
||||
```
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [Jest官方文档](https://jestjs.io/docs/getting-started)
|
||||
- [NestJS测试文档](https://docs.nestjs.com/fundamentals/testing)
|
||||
- [Fastify测试文档](https://www.fastify.io/docs/latest/Guides/Testing/)
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. 为新的controller添加对应的测试文件
|
||||
2. 确保测试覆盖所有的API接口
|
||||
3. 包含成功和失败场景的测试用例
|
||||
4. 使用独立的测试数据,避免测试间的相互影响
|
||||
5. 添加适当的注释和文档
|
580
test/controllers/account.controller.test.ts
Normal file
580
test/controllers/account.controller.test.ts
Normal file
@ -0,0 +1,580 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { getRandomString } from '@/modules/core/helpers';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/user';
|
||||
|
||||
describe('AccountController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let userRepository: UserRepository;
|
||||
let testUser: UserEntity;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试用户
|
||||
const username = getRandomString();
|
||||
testUser = await userRepository.save({
|
||||
username,
|
||||
nickname: 'Test Account User',
|
||||
password: 'password123',
|
||||
email: 'testaccount@example.com',
|
||||
});
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testUser) {
|
||||
await userRepository.remove(testUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('POST /account/register', () => {
|
||||
it('should register user successfully', async () => {
|
||||
const username = getRandomString();
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username,
|
||||
nickname: 'New User',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newUser = result.json();
|
||||
expect(newUser.username).toBe(username);
|
||||
expect(newUser.nickname).toBe('New User');
|
||||
// 不应该返回密码
|
||||
expect(newUser.password).toBeUndefined();
|
||||
|
||||
// 清理创建的用户
|
||||
await userRepository.delete(newUser.id);
|
||||
});
|
||||
|
||||
it('should fail with missing required fields', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
// missing username and password
|
||||
nickname: 'Test User',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
const response = result.json();
|
||||
expect(response.message).toContain('用户名长度必须为4到30');
|
||||
expect(response.message).toContain('密码长度不得少于8');
|
||||
});
|
||||
|
||||
it('should fail with duplicate username', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username: testUser.username, // 使用已存在的用户名
|
||||
nickname: 'Another User',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('该用户名已被注册');
|
||||
});
|
||||
|
||||
it('should fail with invalid email format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username: 'test_user_email',
|
||||
nickname: 'Test User',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
email: 'invalid-email',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('property email should not exist');
|
||||
});
|
||||
|
||||
it('should fail with password mismatch', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username: 'test_user_pwd',
|
||||
nickname: 'Test User',
|
||||
password: 'password123',
|
||||
plainPassword: 'different_password',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('两次输入密码不同');
|
||||
});
|
||||
|
||||
it('should fail with short password', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username: 'test_user_short',
|
||||
nickname: 'Test User',
|
||||
password: '123',
|
||||
plainPassword: '123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('密码长度不得少于8');
|
||||
});
|
||||
|
||||
it('should fail with long username', async () => {
|
||||
const username = getRandomString(52);
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username,
|
||||
nickname: 'Test User',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('用户名长度必须为4到30');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /account/login', () => {
|
||||
it('should login successfully with username', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.username,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const response = result.json();
|
||||
expect(response.token).toBeDefined();
|
||||
expect(typeof response.token).toBe('string');
|
||||
|
||||
// 保存token用于后续测试
|
||||
authToken = response.token;
|
||||
});
|
||||
|
||||
it('should login successfully with email', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.email,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const response = result.json();
|
||||
expect(response.token).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail with wrong password', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.username,
|
||||
password: 'wrong-password',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
expect(result.json().message).toContain('Unauthorized');
|
||||
});
|
||||
|
||||
it('should fail with non-existent user', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: 'non-existent-user',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
expect(result.json().message).toContain('Unauthorized');
|
||||
});
|
||||
|
||||
it('should fail with missing credentials', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
// missing credential and password
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(502);
|
||||
const response = result.json();
|
||||
expect(response.message).toContain('登录凭证不得为空');
|
||||
expect(response.message).toContain('登录凭证长度必须为4到30');
|
||||
});
|
||||
|
||||
it('should fail with empty credential', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: '',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(502);
|
||||
expect(result.json().message).toContain('登录凭证不得为空');
|
||||
});
|
||||
|
||||
it('should fail with empty password', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.username,
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(502);
|
||||
expect(result.json().message).toContain('密码长度不得少于8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /account/logout', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/logout`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should logout successfully', async () => {
|
||||
const username = getRandomString();
|
||||
const result1 = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/register`,
|
||||
body: {
|
||||
username,
|
||||
nickname: 'New User',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
},
|
||||
});
|
||||
const newUser = result1.json();
|
||||
|
||||
// 首先登录获取token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: username,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
const { token } = loginResult.json();
|
||||
console.log(token);
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/logout`,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
// 清理创建的用户
|
||||
await userRepository.delete(newUser.id);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/logout`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /account/profile', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/account/profile`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return user profile successfully', async () => {
|
||||
// 确保有有效的token
|
||||
if (!authToken) {
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.username,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
}
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/account/profile`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const profile = result.json();
|
||||
expect(profile.id).toBe(testUser.id);
|
||||
expect(profile.username).toBe(testUser.username);
|
||||
expect(profile.nickname).toBe(testUser.nickname);
|
||||
expect(profile.email).toBe(testUser.email);
|
||||
// 不应该返回密码
|
||||
expect(profile.password).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/account/profile`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /account', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account`,
|
||||
body: {
|
||||
nickname: 'Updated Nickname',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update account info successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
nickname: 'Updated Test Account',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedUser = result.json();
|
||||
expect(updatedUser.nickname).toBe('Updated Test Account');
|
||||
expect(updatedUser.id).toBe(testUser.id);
|
||||
});
|
||||
|
||||
it('should update username successfully', async () => {
|
||||
const randomTag = getRandomString(10);
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
username: `updated-account-${randomTag}`,
|
||||
},
|
||||
});
|
||||
console.log(result.json());
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedUser = result.json();
|
||||
expect(updatedUser.username).toBe(`updated-account-${randomTag}`);
|
||||
testUser.username = `updated-account-${randomTag}`;
|
||||
});
|
||||
|
||||
it('should fail with duplicate username', async () => {
|
||||
// 创建另一个用户
|
||||
const username = `another-account-${getRandomString()}`;
|
||||
const anotherUser = await userRepository.save({
|
||||
username,
|
||||
nickname: 'Another Account',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
username, // 尝试使用已存在的用户名
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('该用户名已被注册');
|
||||
|
||||
// 清理
|
||||
await userRepository.remove(anotherUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /account/change-password', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account/change-password`,
|
||||
body: {
|
||||
oldPassword: 'password123',
|
||||
password: 'newpassword123',
|
||||
plainPassword: 'newpassword123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should change password successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account/change-password`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
oldPassword: 'password123',
|
||||
password: 'newpassword123',
|
||||
plainPassword: 'newpassword123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证新密码可以登录
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/login`,
|
||||
body: {
|
||||
credential: testUser.username,
|
||||
password: 'newpassword123',
|
||||
},
|
||||
});
|
||||
expect(loginResult.statusCode).toBe(201);
|
||||
|
||||
// 恢复原密码以便其他测试
|
||||
await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account/change-password`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
oldPassword: 'newpassword123',
|
||||
password: 'password123',
|
||||
plainPassword: 'password123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with wrong old password', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account/change-password`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
oldPassword: 'wrongpassword',
|
||||
password: 'newpassword123',
|
||||
plainPassword: 'newpassword123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(403);
|
||||
expect(result.json().message).toContain('old password do not match');
|
||||
});
|
||||
|
||||
it('should fail with password mismatch', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/account/change-password`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
oldPassword: 'password123',
|
||||
password: 'newpassword123',
|
||||
plainPassword: 'differentpassword',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('两次输入密码不同');
|
||||
});
|
||||
});
|
||||
});
|
211
test/controllers/category.controller.test.ts
Normal file
211
test/controllers/category.controller.test.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository } from '@/modules/content/repositories';
|
||||
import { CategoryService } from '@/modules/content/services';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/content';
|
||||
|
||||
describe('CategoryController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let categoryService: CategoryService;
|
||||
let testCategories: CategoryEntity[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
categoryService = app.get<CategoryService>(CategoryService);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试分类数据
|
||||
const rootCategory = await categoryRepository.save({
|
||||
name: 'Test Root Category',
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
const childCategory = await categoryRepository.save({
|
||||
name: 'Test Child Category',
|
||||
parent: rootCategory,
|
||||
customOrder: 2,
|
||||
});
|
||||
|
||||
testCategories = [rootCategory, childCategory];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testCategories && testCategories.length > 0) {
|
||||
await categoryService.delete(testCategories.map(({ id }) => id));
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /category/tree', () => {
|
||||
it('should return category tree successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/tree`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const categories = result.json();
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return categories with tree structure', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/tree`,
|
||||
});
|
||||
|
||||
const categories = result.json();
|
||||
const rootCategory = categories.find((c: any) => c.name === 'Test Root Category');
|
||||
expect(rootCategory).toBeDefined();
|
||||
expect(rootCategory.children).toBeDefined();
|
||||
expect(Array.isArray(rootCategory.children)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /category', () => {
|
||||
it('should return paginated categories successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return categories with valid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?page=1&limit=10`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(10);
|
||||
});
|
||||
|
||||
it('should fail with invalid page parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?page=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should fail with invalid limit parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The number of data displayed per page must be greater than 1.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle large page numbers gracefully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?page=999999`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle large limit values', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?limit=1000`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.perPage).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /category/:id', () => {
|
||||
it('should return category detail successfully', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/${category.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const categoryDetail = result.json();
|
||||
expect(categoryDetail.id).toBe(category.id);
|
||||
expect(categoryDetail.name).toBe(category.name);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should fail with non-existent category ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should return category with children if exists', async () => {
|
||||
const rootCategory = testCategories.find((c) => !c.parent);
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/${rootCategory.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const categoryDetail = result.json();
|
||||
expect(categoryDetail.children).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
348
test/controllers/comment.controller.test.ts
Normal file
348
test/controllers/comment.controller.test.ts
Normal file
@ -0,0 +1,348 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity, CommentEntity, PostEntity } from '@/modules/content/entities';
|
||||
import {
|
||||
CategoryRepository,
|
||||
CommentRepository,
|
||||
PostRepository,
|
||||
} from '@/modules/content/repositories';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/content';
|
||||
|
||||
describe('CommentController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let commentRepository: CommentRepository;
|
||||
let postRepository: PostRepository;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testComments: CommentEntity[];
|
||||
let testPost: PostEntity;
|
||||
let testCategory: CategoryEntity;
|
||||
let testUser: UserEntity;
|
||||
let authToken: string;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
commentRepository = app.get<CommentRepository>(CommentRepository);
|
||||
postRepository = app.get<PostRepository>(PostRepository);
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'comment.create' },
|
||||
});
|
||||
testUser = await userRepository.save({
|
||||
username: 'testuser_comment',
|
||||
nickname: 'Test User Comment',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'testuser_comment',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试分类
|
||||
testCategory = await categoryRepository.save({
|
||||
name: 'Test Comment Category',
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
// 创建测试文章
|
||||
testPost = await postRepository.save({
|
||||
title: 'Test Post for Comments',
|
||||
body: 'This is a test post for comments.',
|
||||
summary: 'Test post summary',
|
||||
author: testUser,
|
||||
category: testCategory,
|
||||
publishedAt: new Date(),
|
||||
});
|
||||
|
||||
// 创建测试评论
|
||||
const parentComment = await commentRepository.save({
|
||||
body: 'This is a parent comment.',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
|
||||
const childComment = await commentRepository.save({
|
||||
body: 'This is a child comment.',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
parent: parentComment,
|
||||
});
|
||||
|
||||
testComments = [parentComment, childComment];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testComments && testComments.length > 0) {
|
||||
await commentRepository.remove(testComments);
|
||||
}
|
||||
if (testPost) {
|
||||
await postRepository.remove(testPost);
|
||||
}
|
||||
if (testCategory) {
|
||||
await categoryRepository.remove(testCategory);
|
||||
}
|
||||
if (testUser) {
|
||||
await userRepository.remove(testUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /comment/tree', () => {
|
||||
it('should return comment tree for a post', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment/tree?post=${testPost.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const comments = result.json();
|
||||
expect(Array.isArray(comments)).toBe(true);
|
||||
|
||||
// 应该包含父评论和子评论的树形结构
|
||||
const parentComment = comments.find((c: any) => !c.parent);
|
||||
expect(parentComment).toBeDefined();
|
||||
expect(parentComment.children).toBeDefined();
|
||||
expect(Array.isArray(parentComment.children)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail with invalid post UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment/tree?post=invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should handle missing post parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment/tree`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /comment', () => {
|
||||
it('should return paginated comments', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter comments by post', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment?post=${testPost.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
response.items.forEach((comment: any) => {
|
||||
expect(comment.post.id).toBe(testPost.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return comments with pagination', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment?page=1&limit=5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comment?page=0&limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /comment', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
body: {
|
||||
body: 'New test comment',
|
||||
post: testPost.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create comment successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
body: 'New test comment',
|
||||
post: testPost.id,
|
||||
},
|
||||
});
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newComment = result.json();
|
||||
expect(newComment.body).toBe('New test comment');
|
||||
expect(newComment.post.id).toBe(testPost.id);
|
||||
|
||||
// 清理创建的评论
|
||||
await commentRepository.delete(newComment.id);
|
||||
});
|
||||
|
||||
it('should create reply comment successfully', async () => {
|
||||
const parentComment = testComments[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
body: 'Reply to parent comment',
|
||||
post: testPost.id,
|
||||
parent: parentComment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const replyComment = result.json();
|
||||
expect(replyComment.body).toBe('Reply to parent comment');
|
||||
expect(replyComment.parent.id).toBe(parentComment.id);
|
||||
|
||||
// 清理创建的评论
|
||||
await commentRepository.delete(replyComment.id);
|
||||
});
|
||||
|
||||
it('should fail with missing required fields', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
// missing body and post
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
const response = result.json();
|
||||
expect(response.message).toContain('Comment content cannot be empty');
|
||||
expect(response.message).toContain('The post ID must be specified');
|
||||
});
|
||||
|
||||
it('should fail with invalid post ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
body: 'Test comment',
|
||||
post: 'invalid-uuid',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent post', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
body: 'Test comment',
|
||||
post: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The post does not exist');
|
||||
});
|
||||
|
||||
it('should fail with too long comment body', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/comment`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
body: 'A'.repeat(1001), // 超过1000字符限制
|
||||
post: testPost.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The length of the comment content cannot exceed 1000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
513
test/controllers/manager/category.controller.test.ts
Normal file
513
test/controllers/manager/category.controller.test.ts
Normal file
@ -0,0 +1,513 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository } from '@/modules/content/repositories';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/content';
|
||||
|
||||
describe('CategoryController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testCategories: CategoryEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'category.manage' },
|
||||
});
|
||||
// 创建管理员用户
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_category',
|
||||
nickname: 'Admin Category',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_category',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试分类数据
|
||||
const rootCategory = await categoryRepository.save({
|
||||
name: 'Manager Test Root Category',
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
const childCategory = await categoryRepository.save({
|
||||
name: 'Manager Test Child Category',
|
||||
parent: rootCategory,
|
||||
customOrder: 2,
|
||||
});
|
||||
|
||||
testCategories = [rootCategory, childCategory];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testCategories && testCategories.length > 0) {
|
||||
await categoryRepository.remove(testCategories);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /category/tree', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/tree`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return category tree with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/tree`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const categories = result.json();
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/tree`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /category', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return paginated categories with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /category/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/${category.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return category detail with authentication', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/category/${category.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const categoryDetail = result.json();
|
||||
expect(categoryDetail.id).toBe(category.id);
|
||||
expect(categoryDetail.name).toBe(category.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /category', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
body: {
|
||||
name: 'New Test Category',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create category successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'New Manager Test Category',
|
||||
customOrder: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newCategory = result.json();
|
||||
expect(newCategory.name).toBe('New Manager Test Category');
|
||||
expect(newCategory.customOrder).toBe(10);
|
||||
|
||||
// 清理创建的分类
|
||||
await categoryRepository.delete(newCategory.id);
|
||||
});
|
||||
|
||||
it('should create child category successfully', async () => {
|
||||
const parentCategory = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'New Child Category',
|
||||
parent: parentCategory.id,
|
||||
customOrder: 5,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newCategory = result.json();
|
||||
expect(newCategory.name).toBe('New Child Category');
|
||||
expect(newCategory.parent.id).toBe(parentCategory.id);
|
||||
|
||||
// 清理创建的分类
|
||||
await categoryRepository.delete(newCategory.id);
|
||||
});
|
||||
|
||||
it('should fail with missing name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
customOrder: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The classification name cannot be empty');
|
||||
});
|
||||
|
||||
it('should fail with duplicate name at same level', async () => {
|
||||
const existingCategory = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: existingCategory.name,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The Category names are duplicated');
|
||||
});
|
||||
|
||||
it('should fail with invalid parent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Test Category',
|
||||
parent: 'invalid-uuid',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent parent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Test Category',
|
||||
parent: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The parent category does not exist');
|
||||
});
|
||||
|
||||
it('should fail with negative custom order', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Test Category',
|
||||
customOrder: -1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The sorted value must be greater than 0.');
|
||||
});
|
||||
|
||||
it('should fail with too long name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'A'.repeat(26), // 超过25字符限制
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The length of the category name shall not exceed 25',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /category', () => {
|
||||
it('should require authentication', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
body: {
|
||||
id: category.id,
|
||||
name: 'Updated Category Name',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update category successfully', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: category.id,
|
||||
name: 'Updated Manager Category',
|
||||
customOrder: 99,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedCategory = result.json();
|
||||
expect(updatedCategory.name).toBe('Updated Manager Category');
|
||||
expect(updatedCategory.customOrder).toBe(99);
|
||||
});
|
||||
|
||||
it('should fail with missing ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Category',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The ID must be specified');
|
||||
});
|
||||
|
||||
it('should fail with invalid ID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: 'invalid-uuid',
|
||||
name: 'Updated Category',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/category`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
name: 'Updated Category',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /category/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const category = testCategories[0];
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/category/${category.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete category successfully', async () => {
|
||||
// 创建一个临时分类用于删除测试
|
||||
const tempCategory = await categoryRepository.save({
|
||||
name: 'Temp Category for Delete',
|
||||
customOrder: 999,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/category/${tempCategory.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证分类已被删除
|
||||
const deletedCategory = await categoryRepository.findOne({
|
||||
where: { id: tempCategory.id },
|
||||
});
|
||||
expect(deletedCategory).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/category/invalid-uuid`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail when deleting category with children', async () => {
|
||||
const parentCategory = testCategories.find((c) => !c.parent);
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/category/${parentCategory.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// 应该失败,因为有子分类
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
385
test/controllers/manager/comment.controller.test.ts
Normal file
385
test/controllers/manager/comment.controller.test.ts
Normal file
@ -0,0 +1,385 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity, CommentEntity, PostEntity } from '@/modules/content/entities';
|
||||
import {
|
||||
CategoryRepository,
|
||||
CommentRepository,
|
||||
PostRepository,
|
||||
} from '@/modules/content/repositories';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/content';
|
||||
|
||||
describe('CommentController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let commentRepository: CommentRepository;
|
||||
let postRepository: PostRepository;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testComments: CommentEntity[];
|
||||
let testPost: PostEntity;
|
||||
let testCategory: CategoryEntity;
|
||||
let testUser: UserEntity;
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
commentRepository = app.get<CommentRepository>(CommentRepository);
|
||||
postRepository = app.get<PostRepository>(PostRepository);
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'comment.manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_comment',
|
||||
nickname: 'Admin Comment',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 创建普通用户
|
||||
testUser = await userRepository.save({
|
||||
username: 'testuser_manager_comment',
|
||||
nickname: 'Test User Manager Comment',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_comment',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试分类
|
||||
testCategory = await categoryRepository.save({
|
||||
name: 'Manager Test Comment Category',
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
// 创建测试文章
|
||||
testPost = await postRepository.save({
|
||||
title: 'Manager Test Post for Comments',
|
||||
body: 'This is a manager test post for comments.',
|
||||
summary: 'Manager test post summary',
|
||||
author: testUser,
|
||||
category: testCategory,
|
||||
publishedAt: new Date(),
|
||||
});
|
||||
|
||||
// 创建测试评论
|
||||
const parentComment = await commentRepository.save({
|
||||
body: 'This is a manager parent comment.',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
|
||||
const childComment = await commentRepository.save({
|
||||
body: 'This is a manager child comment.',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
parent: parentComment,
|
||||
});
|
||||
|
||||
testComments = [parentComment, childComment];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testComments && testComments.length > 0) {
|
||||
await commentRepository.remove(testComments);
|
||||
}
|
||||
if (testPost) {
|
||||
await postRepository.remove(testPost);
|
||||
}
|
||||
if (testCategory) {
|
||||
await categoryRepository.remove(testCategory);
|
||||
}
|
||||
if (testUser) {
|
||||
await userRepository.remove(testUser);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /comments', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return paginated comments with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter comments by post', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments?post=${testPost.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
response.items.forEach((comment: any) => {
|
||||
expect(comment.post.id).toBe(testPost.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /comments/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const comment = testComments[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/comments/${comment.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /comments', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
body: {
|
||||
ids: [testComments[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete comments successfully', async () => {
|
||||
// 创建临时评论用于删除测试
|
||||
const tempComment = await commentRepository.save({
|
||||
body: 'Temp comment for delete',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempComment.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证评论已被删除
|
||||
const deletedComment = await commentRepository.findOne({
|
||||
where: { id: tempComment.id },
|
||||
});
|
||||
expect(deletedComment).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete multiple comments successfully', async () => {
|
||||
// 创建多个临时评论用于删除测试
|
||||
const tempComment1 = await commentRepository.save({
|
||||
body: 'Temp comment 1 for delete',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
const tempComment2 = await commentRepository.save({
|
||||
body: 'Temp comment 2 for delete',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempComment1.id, tempComment2.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证评论已被删除
|
||||
const deletedComment1 = await commentRepository.findOne({
|
||||
where: { id: tempComment1.id },
|
||||
});
|
||||
const deletedComment2 = await commentRepository.findOne({
|
||||
where: { id: tempComment2.id },
|
||||
});
|
||||
expect(deletedComment1).toBeNull();
|
||||
expect(deletedComment2).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with empty ids array', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [],
|
||||
},
|
||||
});
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID in ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: ['invalid-uuid'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should delete parent comment and its children', async () => {
|
||||
// 创建父评论和子评论用于测试级联删除
|
||||
const parentComment = await commentRepository.save({
|
||||
body: 'Parent comment for cascade delete',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
});
|
||||
|
||||
const childComment = await commentRepository.save({
|
||||
body: 'Child comment for cascade delete',
|
||||
post: testPost,
|
||||
author: testUser,
|
||||
parent: parentComment,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/comments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [parentComment.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证父评论和子评论都被删除
|
||||
const deletedParent = await commentRepository.findOne({
|
||||
where: { id: parentComment.id },
|
||||
});
|
||||
const deletedChild = await commentRepository.findOne({
|
||||
where: { id: childComment.id },
|
||||
});
|
||||
expect(deletedParent).toBeNull();
|
||||
expect(deletedChild).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
253
test/controllers/manager/permission.controller.test.ts
Normal file
253
test/controllers/manager/permission.controller.test.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CommentEntity } from '@/modules/content/entities';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionAction } from '@/modules/rbac/constants';
|
||||
import { PermissionEntity } from '@/modules/rbac/entities';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/rbac';
|
||||
|
||||
describe('PermissionController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let permissionRepository: PermissionRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testPermissions: PermissionEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'system-manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_permission_manager',
|
||||
nickname: 'Admin Permission Manager',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_permission_manager',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试权限数据
|
||||
const permission1 = await permissionRepository.save({
|
||||
name: 'Manager Test Permission 1',
|
||||
label: 'manager.test.permission.1',
|
||||
description: 'Manager test permission description 1',
|
||||
rule: {
|
||||
action: PermissionAction.MANAGE,
|
||||
subject: CommentEntity,
|
||||
},
|
||||
});
|
||||
|
||||
const permission2 = await permissionRepository.save({
|
||||
name: 'Manager Test Permission 2',
|
||||
label: 'manager.test.permission.2',
|
||||
description: 'Manager test permission description 2',
|
||||
rule: {
|
||||
action: PermissionAction.MANAGE,
|
||||
subject: CommentEntity,
|
||||
},
|
||||
});
|
||||
|
||||
const permission3 = await permissionRepository.save({
|
||||
name: 'Manager Test Permission 3',
|
||||
label: 'manager.test.permission.3',
|
||||
rule: {
|
||||
action: PermissionAction.MANAGE,
|
||||
subject: CommentEntity,
|
||||
},
|
||||
});
|
||||
|
||||
testPermissions = [permission1, permission2, permission3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testPermissions && testPermissions.length > 0) {
|
||||
await permissionRepository.remove(testPermissions);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /permissions', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return paginated permissions with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should handle invalid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions?page=0&limit=0`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /permissions/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const permission = testPermissions[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions/${permission.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return permission detail with authentication', async () => {
|
||||
const permission = testPermissions[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions/${permission.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const permissionDetail = result.json();
|
||||
expect(permissionDetail.id).toBe(permission.id);
|
||||
expect(permissionDetail.name).toBe(permission.name);
|
||||
expect(permissionDetail.label).toBe(permission.label);
|
||||
expect(permissionDetail.description).toBe(permission.description);
|
||||
});
|
||||
|
||||
it('should return permission without description if not set', async () => {
|
||||
const permissionWithoutDesc = testPermissions.find((p) => !p.description);
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions/${permissionWithoutDesc.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const permissionDetail = result.json();
|
||||
expect(permissionDetail.id).toBe(permissionWithoutDesc.id);
|
||||
expect(permissionDetail.name).toBe(permissionWithoutDesc.name);
|
||||
expect(permissionDetail.label).toBe(permissionWithoutDesc.label);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions/invalid-uuid`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent permission ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/permissions/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
589
test/controllers/manager/post.controller.test.ts
Normal file
589
test/controllers/manager/post.controller.test.ts
Normal file
@ -0,0 +1,589 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity, PostEntity, TagEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
|
||||
import { PostService } from '@/modules/content/services/post.service';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/content';
|
||||
|
||||
describe('PostController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let postRepository: PostRepository;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let tagRepository: TagRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testPosts: PostEntity[];
|
||||
let testCategory: CategoryEntity;
|
||||
let testTags: TagEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
let postService: PostService;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
postRepository = app.get<PostRepository>(PostRepository);
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
tagRepository = app.get<TagRepository>(TagRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
postService = app.get<PostService>(PostService);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'post.manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_post',
|
||||
nickname: 'Admin Post',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_post',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试分类
|
||||
testCategory = await categoryRepository.save({
|
||||
name: 'Manager Test Post Category',
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
// 创建测试标签
|
||||
const tag1 = await tagRepository.save({
|
||||
name: 'Manager Test Post Tag 1',
|
||||
desc: 'Manager test tag for posts',
|
||||
});
|
||||
|
||||
const tag2 = await tagRepository.save({
|
||||
name: 'Manager Test Post Tag 2',
|
||||
desc: 'Another manager test tag for posts',
|
||||
});
|
||||
|
||||
testTags = [tag1, tag2];
|
||||
|
||||
// 创建测试文章
|
||||
const publishedPost = await postService.create(
|
||||
{
|
||||
title: 'Manager Published Test Post',
|
||||
body: 'This is a manager published test post content.',
|
||||
summary: 'Manager published test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [tag1.id],
|
||||
publish: true,
|
||||
},
|
||||
adminUser,
|
||||
);
|
||||
|
||||
const draftPost = await postService.create(
|
||||
{
|
||||
title: 'Manager Draft Test Post',
|
||||
body: 'This is a manager draft test post content.',
|
||||
summary: 'Manager draft test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [tag2.id],
|
||||
},
|
||||
adminUser,
|
||||
);
|
||||
|
||||
testPosts = [publishedPost, draftPost];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testPosts && testPosts.length > 0) {
|
||||
await postRepository.remove(testPosts);
|
||||
}
|
||||
if (testTags && testTags.length > 0) {
|
||||
await tagRepository.remove(testTags);
|
||||
}
|
||||
if (testCategory) {
|
||||
await categoryRepository.remove(testCategory);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /posts', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return all posts including drafts', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
|
||||
// 应该包含已发布和草稿文章
|
||||
const publishedPosts = response.items.filter((post: any) => post.publishedAt !== null);
|
||||
const draftPosts = response.items.filter((post: any) => post.publishedAt === null);
|
||||
|
||||
expect(publishedPosts.length).toBeGreaterThanOrEqual(0);
|
||||
expect(draftPosts.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should filter posts by category', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?category=${testCategory.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
response.items.forEach((post: any) => {
|
||||
expect(post.category.id).toBe(testCategory.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should search posts by keyword', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?search=Manager Published`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
const foundPost = response.items.find(
|
||||
(post: any) =>
|
||||
post.title.includes('Manager Published') ||
|
||||
post.body.includes('Manager Published'),
|
||||
);
|
||||
expect(foundPost).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /posts/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const post = testPosts[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/${post.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return post detail including drafts', async () => {
|
||||
const draftPost = testPosts.find((p) => p.publishedAt === null);
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/${draftPost.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const postDetail = result.json();
|
||||
expect(postDetail.id).toBe(draftPost.id);
|
||||
expect(postDetail.title).toBe(draftPost.title);
|
||||
expect(postDetail.publishedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/invalid-uuid`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent post ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /posts', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
body: {
|
||||
title: 'New Manager Test Post',
|
||||
body: 'New manager test post content',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create post successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Manager Test Post',
|
||||
body: 'New manager test post content',
|
||||
summary: 'New manager test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [testTags[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newPost = result.json();
|
||||
expect(newPost.title).toBe('New Manager Test Post');
|
||||
expect(newPost.author.id).toBe(adminUser.id);
|
||||
|
||||
// 清理创建的文章
|
||||
await postRepository.delete(newPost.id);
|
||||
});
|
||||
|
||||
it('should create published post', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Published Manager Post',
|
||||
body: 'New published manager post content',
|
||||
summary: 'New published manager post summary',
|
||||
category: testCategory.id,
|
||||
tags: [testTags[0].id],
|
||||
publish: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newPost = result.json();
|
||||
expect(newPost.title).toBe('New Published Manager Post');
|
||||
expect(newPost.publishedAt).not.toBeNull();
|
||||
|
||||
// 清理创建的文章
|
||||
await postRepository.delete(newPost.id);
|
||||
});
|
||||
|
||||
it('should fail with missing required fields', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Manager Test Post',
|
||||
// missing body
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The content of the article must be filled in.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail with invalid category ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Manager Test Post',
|
||||
body: 'New manager test post content',
|
||||
category: 'invalid-uuid',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /posts', () => {
|
||||
it('should require authentication', async () => {
|
||||
const post = testPosts[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
body: {
|
||||
id: post.id,
|
||||
title: 'Updated Post Title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update post successfully', async () => {
|
||||
const post = testPosts[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: post.id,
|
||||
title: 'Updated Manager Post Title',
|
||||
body: 'Updated manager post content',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedPost = result.json();
|
||||
expect(updatedPost.title).toBe('Updated Manager Post Title');
|
||||
expect(updatedPost.body).toBe('Updated manager post content');
|
||||
});
|
||||
|
||||
it('should publish draft post', async () => {
|
||||
const draftPost = testPosts.find((p) => p.publishedAt === null);
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: draftPost.id,
|
||||
publish: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedPost = result.json();
|
||||
expect(updatedPost.publishedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'Updated Post',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The article ID must be specified');
|
||||
});
|
||||
|
||||
it('should fail with non-existent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
title: 'Updated Post',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /posts', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
body: {
|
||||
ids: [testPosts[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete posts successfully', async () => {
|
||||
// 创建临时文章用于删除测试
|
||||
const tempPost = await postRepository.save({
|
||||
title: 'Temp Post for Delete',
|
||||
body: 'Temporary post for deletion test',
|
||||
author: adminUser,
|
||||
category: testCategory,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempPost.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证文章已被删除
|
||||
const deletedPost = await postRepository.findOne({ where: { id: tempPost.id } });
|
||||
expect(deletedPost).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete multiple posts successfully', async () => {
|
||||
// 创建多个临时文章用于删除测试
|
||||
const tempPost1 = await postRepository.save({
|
||||
title: 'Temp Post 1 for Delete',
|
||||
body: 'Temporary post 1 for deletion test',
|
||||
author: adminUser,
|
||||
category: testCategory,
|
||||
});
|
||||
const tempPost2 = await postRepository.save({
|
||||
title: 'Temp Post 2 for Delete',
|
||||
body: 'Temporary post 2 for deletion test',
|
||||
author: adminUser,
|
||||
category: testCategory,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempPost1.id, tempPost2.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证文章已被删除
|
||||
const deletedPost1 = await postRepository.findOne({ where: { id: tempPost1.id } });
|
||||
const deletedPost2 = await postRepository.findOne({ where: { id: tempPost2.id } });
|
||||
expect(deletedPost1).toBeNull();
|
||||
expect(deletedPost2).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with empty ids array', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID in ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: ['invalid-uuid'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
407
test/controllers/manager/role.controller.test.ts
Normal file
407
test/controllers/manager/role.controller.test.ts
Normal file
@ -0,0 +1,407 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { RoleEntity } from '@/modules/rbac/entities';
|
||||
import { PermissionRepository, RoleRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/rbac';
|
||||
|
||||
describe('RoleController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let roleRepository: RoleRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testRoles: RoleEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
roleRepository = app.get<RoleRepository>(RoleRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'role.manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_role_manager',
|
||||
nickname: 'Admin Role Manager',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_role_manager',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试角色数据
|
||||
const role1 = await roleRepository.save({
|
||||
name: 'Manager Test Role 1',
|
||||
label: 'manager-test-role-1',
|
||||
description: 'Manager test role description 1',
|
||||
});
|
||||
|
||||
const role2 = await roleRepository.save({
|
||||
name: 'Manager Test Role 2',
|
||||
label: 'manager-test-role-2',
|
||||
description: 'Manager test role description 2',
|
||||
});
|
||||
|
||||
const role3 = await roleRepository.save({
|
||||
name: 'Manager Test Role 3',
|
||||
label: 'manager-test-role-3',
|
||||
});
|
||||
|
||||
testRoles = [role1, role2, role3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testRoles && testRoles.length > 0) {
|
||||
await roleRepository.remove(testRoles);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('POST /roles', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
body: {
|
||||
name: 'New Test Role',
|
||||
label: 'new-test-role',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create role successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'New Manager Test Role',
|
||||
label: 'new-manager-test-role',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newRole = result.json();
|
||||
expect(newRole.name).toBe('New Manager Test Role');
|
||||
expect(newRole.label).toBe('new-manager-test-role');
|
||||
|
||||
// 清理创建的角色
|
||||
await roleRepository.delete(newRole.id);
|
||||
});
|
||||
|
||||
it('should create role without description', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Role Without Description',
|
||||
label: 'role-without-description',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newRole = result.json();
|
||||
expect(newRole.name).toBe('Role Without Description');
|
||||
expect(newRole.label).toBe('role-without-description');
|
||||
|
||||
// 清理创建的角色
|
||||
await roleRepository.delete(newRole.id);
|
||||
});
|
||||
|
||||
it('should fail with missing name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
label: 'test-role-label',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('名称必须填写');
|
||||
expect(result.json().message).toContain('名称长度最大为100');
|
||||
});
|
||||
|
||||
it('should success with missing label', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Test Role Name',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
});
|
||||
|
||||
it('should success with duplicate name', async () => {
|
||||
const existingRole = testRoles[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: existingRole.name,
|
||||
label: 'different-label',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /roles', () => {
|
||||
it('should require authentication', async () => {
|
||||
const role = testRoles[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
body: {
|
||||
id: role.id,
|
||||
name: 'Updated Role Name',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update role successfully', async () => {
|
||||
const role = testRoles[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: role.id,
|
||||
name: 'Updated Manager Role',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedRole = result.json();
|
||||
expect(updatedRole.name).toBe('Updated Manager Role');
|
||||
});
|
||||
|
||||
it('should fail with missing ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Role',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('ID必须指定');
|
||||
});
|
||||
|
||||
it('should fail with invalid ID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: 'invalid-uuid',
|
||||
name: 'Updated Role',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
name: 'Updated Role',
|
||||
},
|
||||
});
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /roles', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
body: {
|
||||
ids: [testRoles[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete roles successfully', async () => {
|
||||
// 创建临时角色用于删除测试
|
||||
const tempRole = await roleRepository.save({
|
||||
name: 'Temp Role for Delete',
|
||||
label: 'temp-role-for-delete',
|
||||
description: 'Temporary role for deletion test',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempRole.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证角色已被删除
|
||||
const deletedRole = await roleRepository.findOne({ where: { id: tempRole.id } });
|
||||
expect(deletedRole).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete multiple roles successfully', async () => {
|
||||
// 创建多个临时角色用于删除测试
|
||||
const tempRole1 = await roleRepository.save({
|
||||
name: 'Temp Role 1 for Delete',
|
||||
label: 'temp-role-1-for-delete',
|
||||
});
|
||||
const tempRole2 = await roleRepository.save({
|
||||
name: 'Temp Role 2 for Delete',
|
||||
label: 'temp-role-2-for-delete',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempRole1.id, tempRole2.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证角色已被删除
|
||||
const deletedRole1 = await roleRepository.findOne({ where: { id: tempRole1.id } });
|
||||
const deletedRole2 = await roleRepository.findOne({ where: { id: tempRole2.id } });
|
||||
expect(deletedRole1).toBeNull();
|
||||
expect(deletedRole2).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with empty ids array', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID in ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: ['invalid-uuid'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
569
test/controllers/manager/tag.controller.test.ts
Normal file
569
test/controllers/manager/tag.controller.test.ts
Normal file
@ -0,0 +1,569 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { TagEntity } from '@/modules/content/entities';
|
||||
import { TagRepository } from '@/modules/content/repositories';
|
||||
import { getRandomString } from '@/modules/core/helpers';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/content';
|
||||
|
||||
describe('TagController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let tagRepository: TagRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testTags: TagEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let authToken: string;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
tagRepository = app.get<TagRepository>(TagRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'tag.manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_tag',
|
||||
nickname: 'Admin Tag',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_tag',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试标签数据
|
||||
const tag1 = await tagRepository.save({
|
||||
name: 'Manager Test Tag 1',
|
||||
desc: 'Manager test tag description 1',
|
||||
});
|
||||
|
||||
const tag2 = await tagRepository.save({
|
||||
name: 'Manager Test Tag 2',
|
||||
desc: 'Manager test tag description 2',
|
||||
});
|
||||
|
||||
const tag3 = await tagRepository.save({
|
||||
name: 'Manager Test Tag 3',
|
||||
});
|
||||
|
||||
testTags = [tag1, tag2, tag3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testTags && testTags.length > 0) {
|
||||
await tagRepository.remove(testTags);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /tag', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return paginated tags with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /tag/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const tag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/${tag.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return tag detail with authentication', async () => {
|
||||
const tag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/${tag.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const tagDetail = result.json();
|
||||
expect(tagDetail.id).toBe(tag.id);
|
||||
expect(tagDetail.name).toBe(tag.name);
|
||||
expect(tagDetail.desc).toBe(tag.desc);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/invalid-uuid`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /tag', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
body: {
|
||||
name: 'New Test Tag',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create tag successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'New Manager Test Tag',
|
||||
desc: 'New manager test tag description',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newTag = result.json();
|
||||
expect(newTag.name).toBe('New Manager Test Tag');
|
||||
expect(newTag.desc).toBe('New manager test tag description');
|
||||
|
||||
// 清理创建的标签
|
||||
await tagRepository.delete(newTag.id);
|
||||
});
|
||||
|
||||
it('should create tag without description', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Tag Without Description',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newTag = result.json();
|
||||
expect(newTag.name).toBe('Tag Without Description');
|
||||
|
||||
// 清理创建的标签
|
||||
await tagRepository.delete(newTag.id);
|
||||
});
|
||||
|
||||
it('should fail with missing name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
desc: 'Tag description without name',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The classification name cannot be empty');
|
||||
});
|
||||
|
||||
it('should fail with duplicate name', async () => {
|
||||
const existingTag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: existingTag.name,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The label names are repeated');
|
||||
});
|
||||
|
||||
it('should fail with too long name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'A'.repeat(256), // 超过255字符限制
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The maximum length of the label name is 255');
|
||||
});
|
||||
|
||||
it('should fail with too long description', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Valid Tag Name',
|
||||
desc: 'A'.repeat(501), // 超过500字符限制
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The maximum length of the label description is 500',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail with empty name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The classification name cannot be empty');
|
||||
});
|
||||
|
||||
it('should fail with whitespace only name', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: ' ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The classification name cannot be empty');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /tag', () => {
|
||||
it('should require authentication', async () => {
|
||||
const tag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
body: {
|
||||
id: tag.id,
|
||||
name: 'Updated Tag Name',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update tag successfully', async () => {
|
||||
const tag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: tag.id,
|
||||
name: 'Updated Manager Tag',
|
||||
desc: 'Updated manager tag description',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedTag = result.json();
|
||||
expect(updatedTag.name).toBe('Updated Manager Tag');
|
||||
expect(updatedTag.desc).toBe('Updated manager tag description');
|
||||
});
|
||||
|
||||
it('should fail with missing ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Tag',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The ID must be specified');
|
||||
});
|
||||
|
||||
it('should fail with invalid ID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: 'invalid-uuid',
|
||||
name: 'Updated Tag',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: '74e655b3-b69a-42ae-a101-41c224386e74',
|
||||
name: 'Updated Tag',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with duplicate name', async () => {
|
||||
const [tag1, tag2] = testTags;
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: tag1.id,
|
||||
name: tag2.name, // 使用另一个标签的名称
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The label names are repeated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /tag', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
body: {
|
||||
ids: [testTags[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete tags successfully', async () => {
|
||||
// 创建临时标签用于删除测试
|
||||
const tag = getRandomString();
|
||||
const tempTag = await tagRepository.save({
|
||||
name: `Temp Tag for Delete${tag}`,
|
||||
desc: 'Temporary tag for deletion test',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempTag.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证标签已被删除
|
||||
const deletedTag = await tagRepository.findOne({ where: { id: tempTag.id } });
|
||||
expect(deletedTag).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete multiple tags successfully', async () => {
|
||||
// 创建多个临时标签用于删除测试
|
||||
const tag = getRandomString();
|
||||
const tempTag1 = await tagRepository.save({
|
||||
name: `Temp Tag 1 for Delete ${tag}`,
|
||||
});
|
||||
const tempTag2 = await tagRepository.save({
|
||||
name: `Temp Tag 2 for Delete ${tag}`,
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempTag1.id, tempTag2.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证标签已被删除
|
||||
const deletedTag1 = await tagRepository.findOne({ where: { id: tempTag1.id } });
|
||||
const deletedTag2 = await tagRepository.findOne({ where: { id: tempTag2.id } });
|
||||
expect(deletedTag1).toBeNull();
|
||||
expect(deletedTag2).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with empty ids array', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID in ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: ['invalid-uuid'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
547
test/controllers/manager/user.controller.test.ts
Normal file
547
test/controllers/manager/user.controller.test.ts
Normal file
@ -0,0 +1,547 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/manager/manager';
|
||||
|
||||
describe('UserController (Manager)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let userRepository: UserRepository;
|
||||
let testUsers: UserEntity[];
|
||||
let adminUser: UserEntity;
|
||||
let permissionRepository: PermissionRepository;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建管理员用户
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'user.manage' },
|
||||
});
|
||||
adminUser = await userRepository.save({
|
||||
username: 'admin_user_manager',
|
||||
nickname: 'Admin User Manager',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: 'admin_user_manager',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试用户数据
|
||||
const user1 = await userRepository.save({
|
||||
username: 'manager_testuser1',
|
||||
nickname: 'Manager Test User 1',
|
||||
password: 'password123',
|
||||
email: 'manager_testuser1@example.com',
|
||||
});
|
||||
|
||||
const user2 = await userRepository.save({
|
||||
username: 'manager_testuser2',
|
||||
nickname: 'Manager Test User 2',
|
||||
password: 'password123',
|
||||
email: 'manager_testuser2@example.com',
|
||||
});
|
||||
|
||||
const user3 = await userRepository.save({
|
||||
username: 'manager_testuser3',
|
||||
nickname: 'Manager Test User 3',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
testUsers = [user1, user2, user3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testUsers && testUsers.length > 0) {
|
||||
await userRepository.remove(testUsers);
|
||||
}
|
||||
if (adminUser) {
|
||||
await userRepository.remove(adminUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return paginated users with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?page=1&limit=5`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: 'Bearer invalid-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const user = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/${user.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return user detail with authentication', async () => {
|
||||
const user = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/${user.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const userDetail = result.json();
|
||||
expect(userDetail.id).toBe(user.id);
|
||||
expect(userDetail.username).toBe(user.username);
|
||||
expect(userDetail.nickname).toBe(user.nickname);
|
||||
// 管理员应该能看到敏感信息
|
||||
expect(userDetail.email).toBe(user.email);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/invalid-uuid`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent user ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/74e65577-b69a-42ae-a101-41c224386e78`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /users', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
body: {
|
||||
username: 'newmanageruser',
|
||||
nickname: 'New Manager User',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create user successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
username: 'newmanageruser123',
|
||||
nickname: 'New Manager User',
|
||||
password: 'password123',
|
||||
email: 'newmanageruser@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newUser = result.json();
|
||||
expect(newUser.username).toBe('newmanageruser123');
|
||||
expect(newUser.nickname).toBe('New Manager User');
|
||||
expect(newUser.email).toBe('newmanageruser@example.com');
|
||||
// 不应该返回密码
|
||||
expect(newUser.password).toBeUndefined();
|
||||
|
||||
// 清理创建的用户
|
||||
await userRepository.delete(newUser.id);
|
||||
});
|
||||
|
||||
it('should fail with missing required fields', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
// missing username and password
|
||||
nickname: 'Test User',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
const response = result.json();
|
||||
expect(response.message).toContain('用户名长度必须为4到30');
|
||||
expect(response.message).toContain('密码长度不得少于8');
|
||||
});
|
||||
|
||||
it('should fail with duplicate username', async () => {
|
||||
const existingUser = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
username: existingUser.username,
|
||||
nickname: 'Another User',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('该用户名已被注册');
|
||||
});
|
||||
|
||||
it('should fail with invalid email format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
username: 'testuser_email_manager',
|
||||
nickname: 'Test User',
|
||||
password: 'password123',
|
||||
email: 'invalid-email',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('邮箱地址格式错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /users', () => {
|
||||
it('should require authentication', async () => {
|
||||
const user = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
body: {
|
||||
id: user.id,
|
||||
nickname: 'Updated Nickname',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should update user successfully', async () => {
|
||||
const user = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: user.id,
|
||||
nickname: 'Updated Manager User',
|
||||
email: 'updated@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedUser = result.json();
|
||||
expect(updatedUser.nickname).toBe('Updated Manager User');
|
||||
expect(updatedUser.email).toBe('updated@example.com');
|
||||
});
|
||||
|
||||
it('should fail with missing ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
nickname: 'Updated User',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('用户ID格式不正确');
|
||||
});
|
||||
|
||||
it('should fail with invalid ID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: 'invalid-uuid',
|
||||
nickname: 'Updated User',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: '88e677b3-b88a-42ae-a101-88c224386e88',
|
||||
nickname: 'Updated User',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should fail with duplicate username', async () => {
|
||||
const [user1, user2] = testUsers;
|
||||
const result = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
id: user1.id,
|
||||
username: user2.username, // 使用另一个用户的用户名
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('该用户名已被注册');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /users', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
body: {
|
||||
ids: [testUsers[0].id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should delete users successfully', async () => {
|
||||
// 创建临时用户用于删除测试
|
||||
const tempUser = await userRepository.save({
|
||||
username: 'temp_user_for_delete',
|
||||
nickname: 'Temp User for Delete',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempUser.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证用户已被删除
|
||||
const deletedUser = await userRepository.findOne({ where: { id: tempUser.id } });
|
||||
expect(deletedUser).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete multiple users successfully', async () => {
|
||||
// 创建多个临时用户用于删除测试
|
||||
const tempUser1 = await userRepository.save({
|
||||
username: 'temp_user1_for_delete',
|
||||
nickname: 'Temp User 1 for Delete',
|
||||
password: 'password123',
|
||||
});
|
||||
const tempUser2 = await userRepository.save({
|
||||
username: 'temp_user2_for_delete',
|
||||
nickname: 'Temp User 2 for Delete',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [tempUser1.id, tempUser2.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
// 验证用户已被删除
|
||||
const deletedUser1 = await userRepository.findOne({ where: { id: tempUser1.id } });
|
||||
const deletedUser2 = await userRepository.findOne({ where: { id: tempUser2.id } });
|
||||
expect(deletedUser1).toBeNull();
|
||||
expect(deletedUser2).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with missing ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with empty ids array', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID in ids', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: ['invalid-uuid'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should not delete admin user (self-protection)', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
ids: [adminUser.id],
|
||||
},
|
||||
});
|
||||
|
||||
// 应该失败或者有特殊处理,防止管理员删除自己
|
||||
expect([400, 403, 422]).toContain(result.statusCode);
|
||||
});
|
||||
});
|
||||
});
|
428
test/controllers/post.controller.test.ts
Normal file
428
test/controllers/post.controller.test.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { CategoryEntity, PostEntity, TagEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
|
||||
import { PostService } from '@/modules/content/services/post.service';
|
||||
import { getRandomString } from '@/modules/core/helpers';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { PermissionRepository } from '@/modules/rbac/repositories';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/content';
|
||||
|
||||
describe('PostController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let postRepository: PostRepository;
|
||||
let categoryRepository: CategoryRepository;
|
||||
let tagRepository: TagRepository;
|
||||
let userRepository: UserRepository;
|
||||
let testPosts: PostEntity[];
|
||||
let testCategory: CategoryEntity;
|
||||
let testTags: TagEntity[];
|
||||
let testUser: UserEntity;
|
||||
let authToken: string;
|
||||
const cleanData = true;
|
||||
let postService: PostService;
|
||||
let permissionRepository: PermissionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
permissionRepository = app.get<PermissionRepository>(PermissionRepository);
|
||||
postRepository = app.get<PostRepository>(PostRepository);
|
||||
categoryRepository = app.get<CategoryRepository>(CategoryRepository);
|
||||
tagRepository = app.get<TagRepository>(TagRepository);
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
postService = app.get<PostService>(PostService);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const tag = cleanData ? '' : getRandomString();
|
||||
const permission = await permissionRepository.findOneOrFail({
|
||||
where: { name: 'post.create' },
|
||||
});
|
||||
// 创建测试用户
|
||||
testUser = await userRepository.save({
|
||||
username: `test_user_post_${tag}`,
|
||||
nickname: 'Test User Post',
|
||||
password: 'password123',
|
||||
permissions: [permission],
|
||||
});
|
||||
|
||||
// 获取认证token
|
||||
const loginResult = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/user/account/login',
|
||||
body: {
|
||||
credential: `test_user_post_${tag}`,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
authToken = loginResult.json().token;
|
||||
|
||||
// 创建测试分类
|
||||
testCategory = await categoryRepository.save({
|
||||
name: `Test Post Category ${tag}`,
|
||||
customOrder: 1,
|
||||
});
|
||||
|
||||
// 创建测试标签
|
||||
const tag1 = await tagRepository.save({
|
||||
name: `Test Post Tag 1 ${tag}`,
|
||||
desc: 'Test tag for posts',
|
||||
});
|
||||
|
||||
const tag2 = await tagRepository.save({
|
||||
name: `Test Post Tag 2 ${tag}`,
|
||||
desc: 'Another test tag for posts',
|
||||
});
|
||||
|
||||
testTags = [tag1, tag2];
|
||||
|
||||
// 创建测试文章
|
||||
const publishedPost = await postService.create(
|
||||
{
|
||||
title: 'Published Test Post',
|
||||
body: 'This is a published test post content.',
|
||||
summary: 'Published test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [tag1.id],
|
||||
publish: true,
|
||||
},
|
||||
testUser,
|
||||
);
|
||||
|
||||
const draftPost = await postService.create(
|
||||
{
|
||||
title: 'Draft Test Post',
|
||||
body: 'This is a draft test post content.',
|
||||
summary: 'Draft test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [tag2.id],
|
||||
},
|
||||
testUser,
|
||||
);
|
||||
|
||||
testPosts = [publishedPost, draftPost];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (!cleanData) {
|
||||
return;
|
||||
}
|
||||
if (testPosts && testPosts.length > 0) {
|
||||
await postRepository.remove(testPosts);
|
||||
}
|
||||
if (testTags && testTags.length > 0) {
|
||||
await tagRepository.remove(testTags);
|
||||
}
|
||||
if (testCategory) {
|
||||
await categoryRepository.remove(testCategory);
|
||||
}
|
||||
if (testUser) {
|
||||
await userRepository.remove(testUser);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /posts', () => {
|
||||
it('should return published posts only', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
|
||||
// 应该只返回已发布的文章
|
||||
response.items.forEach((post: any) => {
|
||||
expect(post.publishedAt).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return posts with pagination', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?page=1&limit=5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should filter posts by category', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?category=${testCategory.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
response.items.forEach((post: any) => {
|
||||
expect(post.category.id).toBe(testCategory.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should search posts by keyword', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?search=Published`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
// 搜索结果应该包含关键词
|
||||
const foundPost = response.items.find(
|
||||
(post: any) => post.title.includes('Published') || post.body.includes('Published'),
|
||||
);
|
||||
expect(foundPost).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail with invalid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts?page=0&limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /posts/owner', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return user own posts with authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
|
||||
// 应该只返回当前用户的文章
|
||||
response.items.forEach((post: any) => {
|
||||
expect(post.author.id).toBe(testUser.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return both published and draft posts for owner', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
|
||||
// 应该包含已发布和草稿文章
|
||||
const publishedPosts = response.items.filter((post: any) => post.publishedAt !== null);
|
||||
const draftPosts = response.items.filter((post: any) => post.publishedAt === null);
|
||||
|
||||
expect(publishedPosts.length).toBeGreaterThan(0);
|
||||
expect(draftPosts.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /posts/:id', () => {
|
||||
it('should return published post detail', async () => {
|
||||
const publishedPost = testPosts.find((p) => p.publishedAt !== null);
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/${publishedPost.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const postDetail = result.json();
|
||||
expect(postDetail.id).toBe(publishedPost.id);
|
||||
expect(postDetail.title).toBe(publishedPost.title);
|
||||
expect(postDetail.publishedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not return draft post to public', async () => {
|
||||
const draftPost = testPosts.find((p) => p.publishedAt === null);
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/${draftPost.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail with non-existent post ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /posts/owner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const post = testPosts[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner/${post.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should return owner post detail including drafts', async () => {
|
||||
const draftPost = testPosts.find((p) => p.publishedAt === null);
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner/${draftPost.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const postDetail = result.json();
|
||||
expect(postDetail.id).toBe(draftPost.id);
|
||||
expect(postDetail.author.id).toBe(testUser.id);
|
||||
});
|
||||
|
||||
it('should fail when accessing other user post', async () => {
|
||||
// 这里需要创建另一个用户的文章来测试权限
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/posts/owner/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /posts', () => {
|
||||
it('should require authentication', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
body: {
|
||||
title: 'New Test Post',
|
||||
body: 'New test post content',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should create post successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Test Post',
|
||||
body: 'New test post content',
|
||||
summary: 'New test post summary',
|
||||
category: testCategory.id,
|
||||
tags: [testTags[0].id],
|
||||
},
|
||||
});
|
||||
expect(result.statusCode).toBe(201);
|
||||
const newPost = result.json();
|
||||
expect(newPost.title).toBe('New Test Post');
|
||||
expect(newPost.author.id).toBe(testUser.id);
|
||||
|
||||
// 清理创建的文章
|
||||
await postRepository.delete(newPost.id);
|
||||
});
|
||||
|
||||
it('should fail with missing required fields', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Test Post',
|
||||
// missing body
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The content of the article must be filled in.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail with invalid category ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/posts`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: {
|
||||
title: 'New Test Post',
|
||||
body: 'New test post content',
|
||||
category: 'invalid-uuid',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
260
test/controllers/role.controller.test.ts
Normal file
260
test/controllers/role.controller.test.ts
Normal file
@ -0,0 +1,260 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { RoleEntity } from '@/modules/rbac/entities';
|
||||
import { RoleRepository } from '@/modules/rbac/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/rbac';
|
||||
|
||||
describe('RoleController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let roleRepository: RoleRepository;
|
||||
let testRoles: RoleEntity[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
roleRepository = app.get<RoleRepository>(RoleRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试角色数据
|
||||
const role1 = await roleRepository.save({
|
||||
name: 'Test Role 1',
|
||||
label: 'test-role-1',
|
||||
description: 'Test role description 1',
|
||||
});
|
||||
|
||||
const role2 = await roleRepository.save({
|
||||
name: 'Test Role 2',
|
||||
label: 'test-role-2',
|
||||
description: 'Test role description 2',
|
||||
});
|
||||
|
||||
const role3 = await roleRepository.save({
|
||||
name: 'Test Role 3',
|
||||
label: 'test-role-3',
|
||||
});
|
||||
|
||||
testRoles = [role1, role2, role3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testRoles && testRoles.length > 0) {
|
||||
await roleRepository.remove(testRoles);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /roles', () => {
|
||||
it('should return paginated roles successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return roles with valid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?page=1&limit=5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should filter roles by trashed status', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?trashed=none`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail with invalid page parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?page=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should fail with invalid limit parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The number of data displayed per page must be greater than 1.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle negative page numbers', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?page=-1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should handle large page numbers gracefully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?page=999999`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return roles with correct structure', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles?limit=1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
const role = response.items[0];
|
||||
expect(role.id).toBeDefined();
|
||||
expect(role.name).toBeDefined();
|
||||
expect(role.label).toBeDefined();
|
||||
expect(typeof role.name).toBe('string');
|
||||
expect(typeof role.label).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /roles/:id', () => {
|
||||
it('should return role detail successfully', async () => {
|
||||
const role = testRoles[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/${role.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const roleDetail = result.json();
|
||||
expect(roleDetail.id).toBe(role.id);
|
||||
expect(roleDetail.name).toBe(role.name);
|
||||
expect(roleDetail.label).toBe(role.label);
|
||||
expect(roleDetail.description).toBe(role.description);
|
||||
});
|
||||
|
||||
it('should return role without description if not set', async () => {
|
||||
const roleWithoutDesc = testRoles.find((r) => !r.description);
|
||||
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/${roleWithoutDesc.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const roleDetail = result.json();
|
||||
expect(roleDetail.id).toBe(roleWithoutDesc.id);
|
||||
expect(roleDetail.name).toBe(roleWithoutDesc.name);
|
||||
expect(roleDetail.label).toBe(roleWithoutDesc.label);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should fail with non-existent role ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
});
|
||||
console.log(result.json());
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should handle empty UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should handle malformed UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/not-a-uuid-at-all`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should handle UUID with wrong format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/12345678-1234-1234-1234-123456789012345`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should return role with permissions if available', async () => {
|
||||
const role = testRoles[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/roles/${role.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const roleDetail = result.json();
|
||||
// 权限字段应该存在(即使为空)
|
||||
expect(roleDetail.permissions).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
240
test/controllers/tag.controller.test.ts
Normal file
240
test/controllers/tag.controller.test.ts
Normal file
@ -0,0 +1,240 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { TagEntity } from '@/modules/content/entities';
|
||||
import { TagRepository } from '@/modules/content/repositories';
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/content';
|
||||
|
||||
describe('TagController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let tagRepository: TagRepository;
|
||||
let testTags: TagEntity[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
tagRepository = app.get<TagRepository>(TagRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试标签数据
|
||||
const tag1 = await tagRepository.save({
|
||||
name: 'Test Tag 1',
|
||||
desc: 'Test tag description 1',
|
||||
});
|
||||
|
||||
const tag2 = await tagRepository.save({
|
||||
name: 'Test Tag 2',
|
||||
desc: 'Test tag description 2',
|
||||
});
|
||||
|
||||
const tag3 = await tagRepository.save({
|
||||
name: 'Test Tag 3',
|
||||
});
|
||||
|
||||
testTags = [tag1, tag2, tag3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testTags && testTags.length > 0) {
|
||||
await tagRepository.remove(testTags);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /tag', () => {
|
||||
it('should return paginated tags successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return tags with valid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?page=1&limit=5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid page parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?page=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should fail with invalid limit parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The number of data displayed per page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should handle negative page numbers', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?page=-1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should handle negative limit values', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?limit=-5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The number of data displayed per page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should handle large page numbers gracefully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?page=999999`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return tags with correct structure', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag?limit=1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
if (response.items.length > 0) {
|
||||
const tag = response.items[0];
|
||||
expect(tag.id).toBeDefined();
|
||||
expect(tag.name).toBeDefined();
|
||||
expect(typeof tag.name).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /tag/:id', () => {
|
||||
it('should return tag detail successfully', async () => {
|
||||
const tag = testTags[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/${tag.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const tagDetail = result.json();
|
||||
expect(tagDetail.id).toBe(tag.id);
|
||||
expect(tagDetail.name).toBe(tag.name);
|
||||
expect(tagDetail.desc).toBe(tag.desc);
|
||||
});
|
||||
|
||||
it('should return tag without description if not set', async () => {
|
||||
const tagWithoutDesc = testTags.find(t => !t.desc);
|
||||
if (tagWithoutDesc) {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/${tagWithoutDesc.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const tagDetail = result.json();
|
||||
expect(tagDetail.id).toBe(tagWithoutDesc.id);
|
||||
expect(tagDetail.name).toBe(tagWithoutDesc.name);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should fail with non-existent tag ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/74e655b3-b69a-42ae-a101-41c224386e74`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should handle empty UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should handle malformed UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/not-a-uuid-at-all`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should handle UUID with wrong format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/tag/12345678-1234-1234-1234-123456789012345`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
});
|
||||
});
|
253
test/controllers/user.controller.test.ts
Normal file
253
test/controllers/user.controller.test.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { createApp } from '@/modules/core/helpers/app';
|
||||
import { App } from '@/modules/core/types';
|
||||
import { UserEntity } from '@/modules/user/entities';
|
||||
import { UserRepository } from '@/modules/user/repositories';
|
||||
|
||||
import { createOptions } from '@/options';
|
||||
|
||||
const URL_PREFIX = '/api/v1/user';
|
||||
|
||||
describe('UserController (App)', () => {
|
||||
let datasource: DataSource;
|
||||
let app: NestFastifyApplication;
|
||||
let userRepository: UserRepository;
|
||||
let testUsers: UserEntity[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const appConfig: App = await createApp(createOptions)();
|
||||
app = appConfig.container;
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
|
||||
userRepository = app.get<UserRepository>(UserRepository);
|
||||
datasource = app.get<DataSource>(DataSource);
|
||||
|
||||
if (!datasource.isInitialized) {
|
||||
await datasource.initialize();
|
||||
}
|
||||
|
||||
// 创建测试数据
|
||||
await setupTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestData();
|
||||
await datasource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
// 创建测试用户数据
|
||||
const user1 = await userRepository.save({
|
||||
username: 'testuser1',
|
||||
nickname: 'Test User 1',
|
||||
password: 'password123',
|
||||
email: 'testuser1@example.com',
|
||||
});
|
||||
|
||||
const user2 = await userRepository.save({
|
||||
username: 'testuser2',
|
||||
nickname: 'Test User 2',
|
||||
password: 'password123',
|
||||
email: 'testuser2@example.com',
|
||||
});
|
||||
|
||||
const user3 = await userRepository.save({
|
||||
username: 'testuser3',
|
||||
nickname: 'Test User 3',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
testUsers = [user1, user2, user3];
|
||||
}
|
||||
|
||||
async function cleanupTestData() {
|
||||
if (testUsers && testUsers.length > 0) {
|
||||
await userRepository.remove(testUsers);
|
||||
}
|
||||
}
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should return paginated users successfully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.meta).toBeDefined();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return users with valid pagination parameters', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?page=1&limit=5`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.meta.currentPage).toBe(1);
|
||||
expect(response.meta.perPage).toBe(5);
|
||||
});
|
||||
|
||||
it('should fail with invalid page parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?page=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should fail with invalid limit parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?limit=0`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain(
|
||||
'The number of data displayed per page must be greater than 1.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle negative page numbers', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?page=-1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('The current page must be greater than 1.');
|
||||
});
|
||||
|
||||
it('should handle large page numbers gracefully', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?page=999999`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return users without sensitive information', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?limit=1`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
|
||||
if (response.items.length > 0) {
|
||||
const user = response.items[0];
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.username).toBeDefined();
|
||||
expect(user.nickname).toBeDefined();
|
||||
// 不应该包含敏感信息
|
||||
expect(user.password).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should filter users by order parameter', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users?orderBy=createdAt`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const response = result.json();
|
||||
expect(Array.isArray(response.items)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should return user detail successfully', async () => {
|
||||
const user = testUsers[0];
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/${user.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const userDetail = result.json();
|
||||
expect(userDetail.id).toBe(user.id);
|
||||
expect(userDetail.username).toBe(user.username);
|
||||
expect(userDetail.nickname).toBe(user.nickname);
|
||||
// 不应该包含敏感信息
|
||||
expect(userDetail.password).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return user with email if available', async () => {
|
||||
const userWithEmail = testUsers.find((u) => u.email);
|
||||
if (userWithEmail) {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/${userWithEmail.id}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
const userDetail = result.json();
|
||||
expect(userDetail.email).toBe(userWithEmail.email);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID format', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/invalid-uuid`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should fail with non-existent user ID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/74e655b3-b99a-42ae-a101-41c224386e74`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should handle empty UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should handle malformed UUID', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/not-a-uuid-at-all`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
|
||||
it('should handle UUID with wrong length', async () => {
|
||||
const result = await app.inject({
|
||||
method: 'GET',
|
||||
url: `${URL_PREFIX}/users/12345678-1234-1234-1234-123456789012345`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.json().message).toContain('Validation failed (uuid is expected)');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user