feat: 重构测试框架并优化代码结构
refactor(tests): 将e2e_tests迁移到tests_suite和api_integration_tests style: 为Java类添加文档注释 docs: 更新.gitignore和配置文件 test: 添加性能测试和Playwright测试脚本 chore: 清理旧测试文件和配置
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# E2E测试环境配置
|
||||
|
||||
# API配置
|
||||
API_BASE_URL=http://localhost:8080
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=55432
|
||||
DATABASE_NAME=manage_system
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=postgres
|
||||
|
||||
# 测试用户凭证
|
||||
TEST_USERNAME=admin
|
||||
TEST_PASSWORD=admin123
|
||||
|
||||
# 浏览器配置
|
||||
HEADLESS_BROWSER=true
|
||||
BROWSER_TYPE=chromium
|
||||
|
||||
# 超时配置(毫秒)
|
||||
REQUEST_TIMEOUT=30000
|
||||
@@ -0,0 +1,50 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
allure-results/
|
||||
allure-report/
|
||||
.tox/
|
||||
.hypothesis/
|
||||
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
reports/
|
||||
test_data/*.json
|
||||
!test_data/.gitkeep
|
||||
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -0,0 +1,522 @@
|
||||
# Tests Suite - 统一测试套件
|
||||
|
||||
## 概述
|
||||
|
||||
tests_suite 是 Novalon 管理系统的统一测试套件项目,包含所有非单元测试:
|
||||
|
||||
- **集成测试**:API集成测试、数据库集成测试、WebSocket集成测试
|
||||
- **E2E测试**:Web UI测试、API E2E测试、业务流程测试
|
||||
- **性能测试**:负载测试、压力测试、尖峰测试
|
||||
- **安全测试**:认证测试、授权测试、注入测试
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
tests_suite/
|
||||
├── README.md # 本文档
|
||||
├── pytest.ini # Pytest配置
|
||||
├── pyproject.toml # 项目配置
|
||||
├── requirements.txt # Python依赖
|
||||
├── .env.example # 环境变量示例
|
||||
├── conftest.py # 全局fixtures和配置
|
||||
│
|
||||
├── config/ # 配置管理
|
||||
│ ├── __init__.py
|
||||
│ └── settings.py # 应用配置
|
||||
│
|
||||
├── tests/ # 测试用例(按类型分层)
|
||||
│ ├── integration/ # 集成测试
|
||||
│ │ ├── api/ # API集成测试
|
||||
│ │ ├── database/ # 数据库集成测试
|
||||
│ │ └── websocket/ # WebSocket集成测试
|
||||
│ ├── e2e/ # E2E测试
|
||||
│ │ ├── web/ # Web UI测试
|
||||
│ │ ├── api/ # API E2E测试
|
||||
│ │ └── business/ # 业务流程测试
|
||||
│ ├── performance/ # 性能测试
|
||||
│ └── security/ # 安全测试
|
||||
│
|
||||
├── test_utils/ # 测试工具和辅助函数
|
||||
│ ├── api_client/ # API客户端封装
|
||||
│ ├── ui_helpers/ # UI辅助函数
|
||||
│ ├── data_manager/ # 测试数据管理
|
||||
│ ├── assertions/ # 自定义断言
|
||||
│ ├── logger.py # 日志工具
|
||||
│ └── data_generator.py # 数据生成器
|
||||
│
|
||||
├── fixtures/ # Pytest fixtures
|
||||
│ ├── api_fixtures.py # API相关fixtures
|
||||
│ ├── db_fixtures.py # 数据库fixtures
|
||||
│ ├── ui_fixtures.py # UI相关fixtures
|
||||
│ └── data_fixtures.py # 测试数据fixtures
|
||||
│
|
||||
├── test_data/ # 测试数据文件
|
||||
└── reports/ # 测试报告
|
||||
├── html/
|
||||
├── performance/
|
||||
└── coverage/
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境准备
|
||||
|
||||
1. **安装Python依赖**
|
||||
```bash
|
||||
cd tests_suite
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **安装Playwright浏览器**
|
||||
```bash
|
||||
playwright install
|
||||
```
|
||||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑.env文件,配置相关环境变量
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
#### 运行所有测试
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
#### 按类型运行测试
|
||||
|
||||
**集成测试**
|
||||
```bash
|
||||
# 运行所有集成测试
|
||||
pytest tests/integration/ -v
|
||||
|
||||
# 运行API集成测试
|
||||
pytest tests/integration/api/ -v
|
||||
|
||||
# 运行数据库集成测试
|
||||
pytest tests/integration/database/ -v
|
||||
|
||||
# 运行WebSocket集成测试
|
||||
pytest tests/integration/websocket/ -v
|
||||
```
|
||||
|
||||
**E2E测试**
|
||||
```bash
|
||||
# 运行所有E2E测试
|
||||
pytest tests/e2e/ -v
|
||||
|
||||
# 运行Web UI测试
|
||||
pytest tests/e2e/web/ -v
|
||||
|
||||
# 运行API E2E测试
|
||||
pytest tests/e2e/api/ -v
|
||||
|
||||
# 运行业务流程测试
|
||||
pytest tests/e2e/business/ -v
|
||||
```
|
||||
|
||||
**性能测试**
|
||||
```bash
|
||||
# 运行所有性能测试
|
||||
pytest tests/performance/ -v
|
||||
|
||||
# 运行负载测试
|
||||
pytest tests/performance/test_load_testing.py -v
|
||||
|
||||
# 运行压力测试
|
||||
pytest tests/performance/test_stress_testing.py -v
|
||||
|
||||
# 运行尖峰测试
|
||||
pytest tests/performance/test_spike_testing.py -v
|
||||
```
|
||||
|
||||
**安全测试**
|
||||
```bash
|
||||
# 运行所有安全测试
|
||||
pytest tests/security/ -v
|
||||
```
|
||||
|
||||
#### 按标记运行测试
|
||||
|
||||
```bash
|
||||
# 运行集成测试
|
||||
pytest tests/ -v -m integration
|
||||
|
||||
# 运行E2E测试
|
||||
pytest tests/ -v -m e2e
|
||||
|
||||
# 运行性能测试
|
||||
pytest tests/ -v -m performance
|
||||
|
||||
# 运行安全测试
|
||||
pytest tests/ -v -m security
|
||||
|
||||
# 运行冒烟测试
|
||||
pytest tests/ -v -m smoke
|
||||
|
||||
# 运行回归测试
|
||||
pytest tests/ -v -m regression
|
||||
```
|
||||
|
||||
#### 运行特定测试
|
||||
|
||||
```bash
|
||||
# 运行单个测试文件
|
||||
pytest tests/integration/api/test_user_api.py -v
|
||||
|
||||
# 运行单个测试函数
|
||||
pytest tests/integration/api/test_user_api.py::test_create_user_success -v
|
||||
|
||||
# 运行单个测试类
|
||||
pytest tests/integration/api/test_user_api.py::TestUserAPI -v
|
||||
```
|
||||
|
||||
### 生成测试报告
|
||||
|
||||
#### HTML测试报告
|
||||
```bash
|
||||
pytest tests/ --html=reports/html/test_report.html --self-contained-html
|
||||
```
|
||||
|
||||
#### 覆盖率报告
|
||||
```bash
|
||||
pytest tests/ --cov=test_utils --cov-report=html --cov-report=term-missing
|
||||
```
|
||||
|
||||
#### Allure测试报告
|
||||
```bash
|
||||
pytest tests/ --alluredir=allure-results
|
||||
allure generate allure-results -o allure-report
|
||||
allure open allure-report
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
在`.env`文件中配置以下环境变量:
|
||||
|
||||
```env
|
||||
ENVIRONMENT=dev
|
||||
|
||||
API_BASE_URL=http://localhost:8080
|
||||
WEB_BASE_URL=http://localhost:3003
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=manage_system
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
TEST_USERNAME=admin
|
||||
TEST_PASSWORD=admin123
|
||||
|
||||
HEADLESS_BROWSER=true
|
||||
BROWSER_TYPE=chromium
|
||||
REQUEST_TIMEOUT=30000
|
||||
```
|
||||
|
||||
### Pytest配置
|
||||
|
||||
主要配置项在`pytest.ini`中:
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=test_utils
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--alluredir=allure-results
|
||||
```
|
||||
|
||||
## 测试分层说明
|
||||
|
||||
### 集成测试
|
||||
|
||||
**目标**:测试模块间的交互和API端点
|
||||
|
||||
**特点**:
|
||||
- 使用真实数据库
|
||||
- 测试API契约
|
||||
- 验证数据持久化
|
||||
|
||||
**工具**:pytest + httpx + testcontainers
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
def test_create_user_api(api_client: Client, test_user_data: dict):
|
||||
response = api_client.post("/api/users", json=test_user_data)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["username"] == test_user_data["username"]
|
||||
```
|
||||
|
||||
### E2E测试
|
||||
|
||||
**目标**:从用户视角验证完整业务流程
|
||||
|
||||
**特点**:
|
||||
- 使用真实浏览器
|
||||
- 模拟用户操作
|
||||
- 验证端到端流程
|
||||
|
||||
**工具**:pytest + playwright
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
@pytest.mark.e2e
|
||||
def test_complete_user_lifecycle(page: Page, web_base_url: str):
|
||||
page.goto(f"{web_base_url}/login")
|
||||
page.fill("input[name='username']", "admin")
|
||||
page.fill("input[name='password']", "admin123")
|
||||
page.click("button[type='submit']")
|
||||
# ... 完整的用户操作流程
|
||||
```
|
||||
|
||||
### 性能测试
|
||||
|
||||
**目标**:验证系统性能指标
|
||||
|
||||
**特点**:
|
||||
- 模拟真实负载
|
||||
- 监控性能指标
|
||||
- 发现性能瓶颈
|
||||
|
||||
**工具**:pytest + locust/k6
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
@pytest.mark.performance
|
||||
def test_load_testing():
|
||||
# 性能测试逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
### 安全测试
|
||||
|
||||
**目标**:发现安全漏洞
|
||||
|
||||
**特点**:
|
||||
- 测试认证授权
|
||||
- 检测注入攻击
|
||||
- 验证安全配置
|
||||
|
||||
**工具**:pytest + 安全工具
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
@pytest.mark.security
|
||||
def test_sql_injection_protection(api_client: Client):
|
||||
malicious_input = "' OR '1'='1"
|
||||
response = api_client.post("/api/auth/login", json={
|
||||
"username": malicious_input,
|
||||
"password": "test"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
```
|
||||
|
||||
## CI/CD集成
|
||||
|
||||
tests_suite 集成到项目的 CI/CD 流水线中:
|
||||
|
||||
### 快速反馈(提交时)
|
||||
```yaml
|
||||
# 单元测试在各自业务项目中运行
|
||||
# tests_suite不参与此阶段
|
||||
```
|
||||
|
||||
### 全面验证(合并前)
|
||||
```yaml
|
||||
# 运行集成测试
|
||||
pytest tests/integration/ -v --tb=short
|
||||
|
||||
# 运行E2E测试
|
||||
pytest tests/e2e/ -v --tb=short
|
||||
```
|
||||
|
||||
### 生产验证(部署前)
|
||||
```yaml
|
||||
# 运行性能测试
|
||||
pytest tests/performance/ -v
|
||||
|
||||
# 运行安全测试
|
||||
pytest tests/security/ -v
|
||||
```
|
||||
|
||||
## 测试数据管理
|
||||
|
||||
### 数据工厂模式
|
||||
|
||||
使用`test_utils/data_manager/data_generator.py`中的数据工厂生成测试数据:
|
||||
|
||||
```python
|
||||
from test_utils.data_manager.data_generator import UserDataFactory
|
||||
|
||||
# 生成用户数据
|
||||
user_data = UserDataFactory.create_user_data()
|
||||
|
||||
# 自定义数据
|
||||
custom_user = UserDataFactory.create_user_data({
|
||||
"username": "test_user",
|
||||
"email": "test@example.com"
|
||||
})
|
||||
```
|
||||
|
||||
### 测试数据清理
|
||||
|
||||
使用fixtures自动清理测试数据:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
def test_create_user(api_client: Client, test_user_data: dict, clean_test_data):
|
||||
# clean_test_data fixture会在测试后自动清理数据
|
||||
response = api_client.post("/api/users", json=test_user_data)
|
||||
assert response.status_code == 201
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 查看详细输出
|
||||
```bash
|
||||
pytest tests/ -v -s
|
||||
```
|
||||
|
||||
### 显示更详细的traceback
|
||||
```bash
|
||||
pytest tests/ -v --tb=long
|
||||
```
|
||||
|
||||
### 在第一个失败时停止
|
||||
```bash
|
||||
pytest tests/ -v -x
|
||||
```
|
||||
|
||||
### 进入pdb调试器
|
||||
```bash
|
||||
pytest tests/ -v --pdb
|
||||
```
|
||||
|
||||
### 非headless模式调试
|
||||
```bash
|
||||
HEADLESS_BROWSER=false pytest tests/e2e/web/ -v
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 测试超时
|
||||
|
||||
**问题**:测试执行超时
|
||||
|
||||
**解决方案**:
|
||||
- 增加请求超时时间:修改`.env`中的`REQUEST_TIMEOUT`
|
||||
- 增加页面默认超时时间:`page.set_default_timeout(60000)`
|
||||
- 增加特定操作的等待时间
|
||||
|
||||
### 2. 405 Method Not Allowed错误
|
||||
|
||||
**问题**:API端点返回405错误
|
||||
|
||||
**解决方案**:
|
||||
- 检查API端点的HTTP方法是否正确
|
||||
- 检查路由配置是否正确
|
||||
- 检查Handler方法的HTTP方法注解
|
||||
|
||||
### 3. 前后端数据不一致
|
||||
|
||||
**问题**:前端显示的数据与API返回的数据不一致
|
||||
|
||||
**解决方案**:
|
||||
- 检查前端是否正确调用API
|
||||
- 检查API返回的数据格式
|
||||
- 检查前端数据渲染逻辑
|
||||
|
||||
### 4. Playwright浏览器未安装
|
||||
|
||||
**问题**:运行E2E测试时报错浏览器未安装
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
playwright install
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 测试命名规范
|
||||
|
||||
- 测试文件:`test_*.py`
|
||||
- 测试类:`Test*`
|
||||
- 测试函数:`test_*`
|
||||
|
||||
### 2. 使用标记
|
||||
|
||||
为测试添加适当的标记:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.smoke
|
||||
def test_create_user_api():
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. 使用fixtures
|
||||
|
||||
充分利用fixtures减少重复代码:
|
||||
|
||||
```python
|
||||
def test_create_user(api_client: Client, auth_headers: dict, test_user_data: dict):
|
||||
response = api_client.post("/api/users",
|
||||
json=test_user_data,
|
||||
headers=auth_headers)
|
||||
assert response.status_code == 201
|
||||
```
|
||||
|
||||
### 4. 数据隔离
|
||||
|
||||
每个测试应该独立运行,不依赖其他测试:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
def test_create_user(api_client: Client, test_user_data: dict, clean_test_data):
|
||||
# clean_test_data fixture确保测试数据清理
|
||||
response = api_client.post("/api/users", json=test_user_data)
|
||||
assert response.status_code == 201
|
||||
```
|
||||
|
||||
### 5. 断言清晰
|
||||
|
||||
使用清晰的断言消息:
|
||||
|
||||
```python
|
||||
assert response.status_code == 201, f"Expected 201, got {response.status_code}"
|
||||
assert "username" in response.json(), "Response should contain username"
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Pytest官方文档](https://docs.pytest.org/)
|
||||
- [Playwright官方文档](https://playwright.dev/python/)
|
||||
- [Httpx官方文档](https://www.python-httpx.org/)
|
||||
- [测试金字塔](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
|
||||
## 维护者
|
||||
|
||||
- **张翔**(全栈质量保障与研发效能工程师)
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 变更说明 |
|
||||
|-----|------|---------|
|
||||
| 1.0.0 | 2026-03-14 | 初始版本,完成测试架构重构 |
|
||||
|
||||
---
|
||||
|
||||
**最后更新时间**: 2026-03-14
|
||||
@@ -0,0 +1,10 @@
|
||||
from .settings import settings, get_settings, Settings, DevSettings, StagingSettings, ProductionSettings
|
||||
|
||||
__all__ = [
|
||||
"settings",
|
||||
"get_settings",
|
||||
"Settings",
|
||||
"DevSettings",
|
||||
"StagingSettings",
|
||||
"ProductionSettings",
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
ENVIRONMENT: str = "dev"
|
||||
|
||||
API_BASE_URL: str = "http://localhost:8080"
|
||||
WEB_BASE_URL: str = "http://localhost:3003"
|
||||
|
||||
DB_HOST: str = "localhost"
|
||||
DB_PORT: int = 5432
|
||||
DB_NAME: str = "manage_system"
|
||||
DB_USERNAME: str = "postgres"
|
||||
DB_PASSWORD: str = "postgres"
|
||||
|
||||
TEST_USERNAME: str = "admin"
|
||||
TEST_PASSWORD: str = "admin123"
|
||||
|
||||
HEADLESS_BROWSER: bool = True
|
||||
BROWSER_TYPE: str = "chromium"
|
||||
REQUEST_TIMEOUT: int = 30000
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
class DevSettings(Settings):
|
||||
ENVIRONMENT: str = "dev"
|
||||
API_BASE_URL: str = "http://localhost:8080"
|
||||
WEB_BASE_URL: str = "http://localhost:3003"
|
||||
|
||||
|
||||
class StagingSettings(Settings):
|
||||
ENVIRONMENT: str = "staging"
|
||||
API_BASE_URL: str = "http://staging-api.example.com"
|
||||
WEB_BASE_URL: str = "http://staging-web.example.com"
|
||||
|
||||
|
||||
class ProductionSettings(Settings):
|
||||
ENVIRONMENT: str = "production"
|
||||
API_BASE_URL: str = "https://api.example.com"
|
||||
WEB_BASE_URL: str = "https://example.com"
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
env = Settings().ENVIRONMENT
|
||||
if env == "production":
|
||||
return ProductionSettings()
|
||||
elif env == "staging":
|
||||
return StagingSettings()
|
||||
else:
|
||||
return DevSettings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
Pytest配置和fixtures
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from typing import AsyncGenerator, Generator
|
||||
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
||||
from playwright.sync_api import sync_playwright
|
||||
from httpx import AsyncClient
|
||||
|
||||
from config import settings
|
||||
from test_utils.data_manager.test_data_manager import TestDataManager
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""创建事件循环"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def async_browser() -> AsyncGenerator[Browser, None]:
|
||||
"""异步浏览器fixture"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=settings.HEADLESS_BROWSER)
|
||||
yield browser
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sync_browser():
|
||||
"""同步浏览器fixture"""
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=settings.HEADLESS_BROWSER)
|
||||
yield browser
|
||||
browser.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_context(async_browser: Browser) -> AsyncGenerator[BrowserContext, None]:
|
||||
"""异步浏览器上下文fixture"""
|
||||
context = await async_browser.new_context(
|
||||
viewport={"width": 1280, "height": 720},
|
||||
locale="zh-CN"
|
||||
)
|
||||
yield context
|
||||
await context.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_page(async_context: BrowserContext) -> AsyncGenerator[Page, None]:
|
||||
"""异步页面fixture"""
|
||||
page = await async_context.new_page()
|
||||
page.set_default_timeout(settings.REQUEST_TIMEOUT)
|
||||
yield page
|
||||
await page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def http_client() -> AsyncGenerator[AsyncClient, None]:
|
||||
"""HTTP客户端fixture"""
|
||||
async with AsyncClient(
|
||||
base_url=settings.API_BASE_URL,
|
||||
timeout=settings.REQUEST_TIMEOUT / 1000
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(http_client: AsyncClient) -> str:
|
||||
"""获取认证token"""
|
||||
response = await http_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"username": settings.TEST_USERNAME,
|
||||
"password": settings.TEST_PASSWORD
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
return data.get("token")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client(http_client: AsyncClient, auth_token: str) -> AsyncClient:
|
||||
"""已认证的HTTP客户端"""
|
||||
http_client.headers.update({"Authorization": f"Bearer {auth_token}"})
|
||||
return http_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_data():
|
||||
"""测试用户数据"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"username": f"testuser_{timestamp}",
|
||||
"password": "Password123!",
|
||||
"email": f"test_{timestamp}@example.com",
|
||||
"roleId": 2,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_role_data():
|
||||
"""测试角色数据"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"roleName": f"TEST_ROLE_{timestamp}",
|
||||
"roleKey": f"test_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_dictionary_data():
|
||||
"""测试字典数据"""
|
||||
return {
|
||||
"type": "USER_STATUS",
|
||||
"code": "ACTIVE",
|
||||
"name": "激活",
|
||||
"value": "1",
|
||||
"remark": "用户激活状态",
|
||||
"sort": 1
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_user(authenticated_client: AsyncClient):
|
||||
"""清理测试用户"""
|
||||
user_ids = []
|
||||
|
||||
yield user_ids
|
||||
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/users/{user_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_role(authenticated_client: AsyncClient):
|
||||
"""清理测试角色"""
|
||||
role_ids = []
|
||||
|
||||
yield role_ids
|
||||
|
||||
for role_id in role_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/roles/{role_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_dictionary(authenticated_client: AsyncClient):
|
||||
"""清理测试字典"""
|
||||
dict_ids = []
|
||||
|
||||
yield dict_ids
|
||||
|
||||
for dict_id in dict_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/dictionaries/{dict_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_dict_type(authenticated_client: AsyncClient):
|
||||
"""清理字典类型"""
|
||||
dict_ids = []
|
||||
|
||||
yield dict_ids
|
||||
|
||||
for dict_id in dict_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/dict/types/{dict_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_config(authenticated_client: AsyncClient):
|
||||
"""清理系统配置"""
|
||||
config_ids = []
|
||||
|
||||
yield config_ids
|
||||
|
||||
for config_id in config_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/config/{config_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_notice(authenticated_client: AsyncClient):
|
||||
"""清理系统公告"""
|
||||
notice_ids = []
|
||||
|
||||
yield notice_ids
|
||||
|
||||
for notice_id in notice_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/notices/{notice_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_file(authenticated_client: AsyncClient):
|
||||
"""清理文件"""
|
||||
file_ids = []
|
||||
|
||||
yield file_ids
|
||||
|
||||
for file_id in file_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/files/{file_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_data_manager(authenticated_client: AsyncClient) -> AsyncGenerator[TestDataManager, None]:
|
||||
"""测试数据管理器fixture"""
|
||||
manager = TestDataManager(authenticated_client)
|
||||
yield manager
|
||||
await manager.cleanup_all()
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
from typing import Generator, Dict, Any
|
||||
from httpx import AsyncClient, Client
|
||||
from playwright.sync_api import Page, BrowserContext
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_base_url() -> str:
|
||||
return settings.API_BASE_URL
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def web_base_url() -> str:
|
||||
return settings.WEB_BASE_URL
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def api_client(api_base_url: str) -> Generator[Client, None, None]:
|
||||
with Client(base_url=api_base_url, timeout=settings.REQUEST_TIMEOUT) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def async_api_client(api_base_url: str) -> Generator[AsyncClient, None, None]:
|
||||
async with AsyncClient(base_url=api_base_url, timeout=settings.REQUEST_TIMEOUT) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def auth_headers(api_client: Client) -> Dict[str, str]:
|
||||
response = api_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"username": settings.TEST_USERNAME,
|
||||
"password": settings.TEST_PASSWORD
|
||||
}
|
||||
)
|
||||
data = response.json()
|
||||
token = data.get("token", data.get("access_token", ""))
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def authenticated_client(api_client: Client, auth_headers: Dict[str, str]) -> Client:
|
||||
api_client.headers.update(auth_headers)
|
||||
return api_client
|
||||
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
from typing import Generator, Dict, Any
|
||||
from faker import Faker
|
||||
from test_utils.data_manager.data_generator import UserDataFactory
|
||||
|
||||
|
||||
fake = Faker("zh_CN")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_user_data() -> Dict[str, Any]:
|
||||
return UserDataFactory.create_user_data()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_role_data() -> Dict[str, Any]:
|
||||
return {
|
||||
"role_name": fake.word(),
|
||||
"role_key": fake.word(),
|
||||
"description": fake.sentence(),
|
||||
"status": "0",
|
||||
"permissions": []
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_menu_data() -> Dict[str, Any]:
|
||||
return {
|
||||
"menu_name": fake.word(),
|
||||
"parent_id": 0,
|
||||
"order_num": fake.random_int(1, 100),
|
||||
"path": f"/{fake.word()}",
|
||||
"component": f"{fake.word()}/{fake.word()}",
|
||||
"menu_type": "C",
|
||||
"visible": "0",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_dict_data() -> Dict[str, Any]:
|
||||
return {
|
||||
"dict_name": fake.word(),
|
||||
"dict_type": fake.word(),
|
||||
"status": "0",
|
||||
"remark": fake.sentence()
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def test_config_data() -> Dict[str, Any]:
|
||||
return {
|
||||
"config_name": fake.word(),
|
||||
"config_key": f"sys.{fake.word()}",
|
||||
"config_value": fake.word(),
|
||||
"config_type": "Y",
|
||||
"remark": fake.sentence()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from typing import Generator
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def db_connection():
|
||||
conn = psycopg2.connect(
|
||||
host=settings.DB_HOST,
|
||||
port=settings.DB_PORT,
|
||||
database=settings.DB_NAME,
|
||||
user=settings.DB_USERNAME,
|
||||
password=settings.DB_PASSWORD,
|
||||
cursor_factory=RealDictCursor
|
||||
)
|
||||
yield conn
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_cursor(db_connection):
|
||||
cursor = db_connection.cursor()
|
||||
yield cursor
|
||||
cursor.close()
|
||||
db_connection.rollback()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def clean_test_data(db_cursor):
|
||||
yield
|
||||
tables = ["sys_user", "sys_role", "sys_menu", "sys_dict", "sys_config"]
|
||||
for table in tables:
|
||||
db_cursor.execute(f"DELETE FROM {table} WHERE username LIKE 'test_%'")
|
||||
db_cursor.connection.commit()
|
||||
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
from typing import Generator
|
||||
from playwright.sync_api import Page, Browser, BrowserContext
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def page(browser: Browser) -> Generator[Page, None, None]:
|
||||
context = browser.new_context(
|
||||
viewport={"width": 1280, "height": 720},
|
||||
locale="zh-CN"
|
||||
)
|
||||
page = context.new_page()
|
||||
page.set_default_timeout(30000)
|
||||
yield page
|
||||
context.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def authenticated_page(page: Page, web_base_url: str) -> Page:
|
||||
page.goto(f"{web_base_url}/login")
|
||||
page.fill("input[name='username']", settings.TEST_USERNAME)
|
||||
page.fill("input[name='password']", settings.TEST_PASSWORD)
|
||||
page.click("button[type='submit']")
|
||||
page.wait_for_url(f"{web_base_url}/**")
|
||||
return page
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3003',
|
||||
trace: 'on-first-retry',
|
||||
headless: true,
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tests-suite"
|
||||
version = "1.0.0"
|
||||
description = "统一测试套件 - Novalon管理系统"
|
||||
authors = [
|
||||
{name = "张翔", email = "zhangxiang@example.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = [
|
||||
"-v",
|
||||
"--strict-markers",
|
||||
"--tb=short",
|
||||
"--cov=test_utils",
|
||||
"--cov-report=html",
|
||||
"--cov-report=term-missing",
|
||||
"--alluredir=allure-results",
|
||||
]
|
||||
markers = [
|
||||
"unit: 单元测试",
|
||||
"integration: 集成测试",
|
||||
"e2e: 端到端测试",
|
||||
"performance: 性能测试",
|
||||
"security: 安全测试",
|
||||
"smoke: 冒烟测试",
|
||||
"regression: 回归测试",
|
||||
"slow: 慢速测试",
|
||||
"playwright: Playwright浏览器自动化测试",
|
||||
"auth: 认证相关测试",
|
||||
"user: 用户管理测试",
|
||||
"role: 角色管理测试",
|
||||
"permission: 权限管理测试",
|
||||
"menu: 菜单管理测试",
|
||||
"websocket: WebSocket实时通信测试",
|
||||
"example: 示例测试",
|
||||
"exception: 异常场景测试",
|
||||
"dictionary: 字典管理测试",
|
||||
"dict: 字典管理测试",
|
||||
"config: 系统配置测试",
|
||||
"audit: 审计日志测试",
|
||||
"notice: 通知公告测试",
|
||||
"file: 文件管理测试",
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["test_utils"]
|
||||
omit = [
|
||||
"*/tests/*",
|
||||
"*/test_*.py",
|
||||
"*/__pycache__/*",
|
||||
"*/site-packages/*",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
"@abstractmethod",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
/(
|
||||
# directories
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
@@ -0,0 +1,39 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
pythonpath = .
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=.
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--alluredir=allure-results
|
||||
markers =
|
||||
unit: 单元测试
|
||||
integration: 集成测试
|
||||
e2e: 端到端测试
|
||||
performance: 性能测试
|
||||
security: 安全测试
|
||||
smoke: 冒烟测试
|
||||
regression: 回归测试
|
||||
slow: 慢速测试
|
||||
playwright: Playwright浏览器自动化测试
|
||||
auth: 认证相关测试
|
||||
user: 用户管理测试
|
||||
role: 角色管理测试
|
||||
permission: 权限管理测试
|
||||
menu: 菜单管理测试
|
||||
websocket: WebSocket实时通信测试
|
||||
example: 示例测试
|
||||
exception: 异常场景测试
|
||||
dictionary: 字典管理测试
|
||||
dict: 字典管理测试
|
||||
config: 系统配置测试
|
||||
audit: 审计日志测试
|
||||
notice: 通知公告测试
|
||||
file: 文件管理测试
|
||||
asyncio_mode = auto
|
||||
@@ -0,0 +1,37 @@
|
||||
# Python依赖包
|
||||
|
||||
# 测试框架
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-xdist==3.5.0
|
||||
pytest-playwright==0.4.3
|
||||
|
||||
# Playwright
|
||||
playwright==1.40.0
|
||||
|
||||
# HTTP客户端
|
||||
httpx==0.25.2
|
||||
requests==2.31.0
|
||||
|
||||
# 数据处理
|
||||
pydantic==2.5.2
|
||||
pydantic-settings==2.1.0
|
||||
faker==20.1.0
|
||||
|
||||
# 数据库(可选,需要PostgreSQL开发库)
|
||||
# psycopg2-binary==2.9.9
|
||||
# testcontainers==3.7.1
|
||||
|
||||
# 配置管理
|
||||
python-dotenv==1.0.0
|
||||
pyyaml==6.0.1
|
||||
|
||||
# 测试报告
|
||||
allure-pytest==2.13.2
|
||||
|
||||
# 工具库
|
||||
loguru==0.7.2
|
||||
|
||||
# 性能测试(可选,需要额外配置)
|
||||
# locust==2.17.0
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""API模块"""
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
审计日志API封装
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class SysLogAPI:
|
||||
"""审计日志API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/logs"
|
||||
|
||||
async def get_login_logs(self) -> Any:
|
||||
"""获取所有登录日志"""
|
||||
return await self.client.get(f"{self.base_path}/login")
|
||||
|
||||
async def get_login_log_by_id(self, log_id: int) -> Any:
|
||||
"""根据ID获取登录日志"""
|
||||
return await self.client.get(f"{self.base_path}/login/{log_id}")
|
||||
|
||||
async def create_login_log(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建登录日志"""
|
||||
return await self.client.post(f"{self.base_path}/login", json=data)
|
||||
|
||||
async def get_exception_logs(self) -> Any:
|
||||
"""获取所有异常日志"""
|
||||
return await self.client.get(f"{self.base_path}/exception")
|
||||
|
||||
async def get_exception_log_by_id(self, log_id: int) -> Any:
|
||||
"""根据ID获取异常日志"""
|
||||
return await self.client.get(f"{self.base_path}/exception/{log_id}")
|
||||
|
||||
async def create_exception_log(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建异常日志"""
|
||||
return await self.client.post(f"{self.base_path}/exception", json=data)
|
||||
|
||||
async def get_login_logs_by_page(self, page: int = 0, size: int = 10,
|
||||
sort: str = "id", order: str = "asc",
|
||||
keyword: str = None) -> Any:
|
||||
"""分页获取登录日志"""
|
||||
params = {"page": page, "size": size, "sort": sort, "order": order}
|
||||
if keyword:
|
||||
params["keyword"] = keyword
|
||||
return await self.client.get(f"{self.base_path}/login/page", params=params)
|
||||
|
||||
async def get_operation_logs_by_page(self, page: int = 0, size: int = 10,
|
||||
sort: str = "id", order: str = "asc",
|
||||
keyword: str = None) -> Any:
|
||||
"""分页获取操作日志"""
|
||||
params = {"page": page, "size": size, "sort": sort, "order": order}
|
||||
if keyword:
|
||||
params["keyword"] = keyword
|
||||
return await self.client.get(f"{self.base_path}/operation/page", params=params)
|
||||
|
||||
async def get_login_log_count(self) -> Any:
|
||||
"""获取登录日志总数"""
|
||||
return await self.client.get(f"{self.base_path}/login/count")
|
||||
|
||||
async def get_operation_log_count(self) -> Any:
|
||||
"""获取操作日志总数"""
|
||||
return await self.client.get(f"{self.base_path}/operation/count")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
认证API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class AuthAPI(BaseAPI):
|
||||
"""认证API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/auth")
|
||||
|
||||
async def login(self, username: str, password: str) -> Response:
|
||||
"""用户登录"""
|
||||
return await self.post("/login", json={
|
||||
"username": username,
|
||||
"password": password
|
||||
})
|
||||
|
||||
async def refresh_token(self, refresh_token: str) -> Response:
|
||||
"""刷新token"""
|
||||
return await self.post("/refresh", json={
|
||||
"refreshToken": refresh_token
|
||||
})
|
||||
|
||||
async def logout(self, token: str) -> Response:
|
||||
"""用户登出"""
|
||||
return await self.post("/logout", headers={
|
||||
"Authorization": f"Bearer {token}"
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
基础API类
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class BaseAPI:
|
||||
"""基础API类"""
|
||||
|
||||
def __init__(self, client: AsyncClient, base_url: str = ""):
|
||||
self.client = client
|
||||
self.base_url = base_url
|
||||
|
||||
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""GET请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"GET {url} - Params: {params}")
|
||||
response = await self.client.get(url, params=params, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""POST请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"POST {url} - Data: {data} - JSON: {json}")
|
||||
response = await self.client.post(url, data=data, json=json, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""PUT请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"PUT {url} - Data: {data} - JSON: {json}")
|
||||
response = await self.client.put(url, data=data, json=json, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def delete(self, endpoint: str, **kwargs) -> Response:
|
||||
"""DELETE请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"DELETE {url}")
|
||||
response = await self.client.delete(url, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def assert_status_code(self, response: Response, expected_status: int):
|
||||
"""断言状态码"""
|
||||
assert response.status_code == expected_status, f"Expected {expected_status}, got {response.status_code}. Response: {response.text}"
|
||||
|
||||
async def assert_response_contains(self, response: Response, key: str, value: Any = None):
|
||||
"""断言响应包含指定字段"""
|
||||
data = response.json()
|
||||
assert key in data, f"Response does not contain key '{key}'"
|
||||
if value is not None:
|
||||
assert data[key] == value, f"Expected {value}, got {data[key]}"
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
系统配置API封装
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class SysConfigAPI:
|
||||
"""系统参数配置API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/config"
|
||||
|
||||
async def get_all(self) -> Any:
|
||||
"""获取所有配置"""
|
||||
return await self.client.get(self.base_path)
|
||||
|
||||
async def get_by_key(self, config_key: str) -> Any:
|
||||
"""根据key获取配置"""
|
||||
return await self.client.get(f"{self.base_path}/key/{config_key}")
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建配置"""
|
||||
return await self.client.post(self.base_path, json=data)
|
||||
|
||||
async def update(self, config_id: int, data: Dict[str, Any]) -> Any:
|
||||
"""更新配置"""
|
||||
return await self.client.put(f"{self.base_path}/{config_id}", json=data)
|
||||
|
||||
async def delete(self, config_id: int) -> Any:
|
||||
"""删除配置"""
|
||||
return await self.client.delete(f"{self.base_path}/{config_id}")
|
||||
|
||||
async def refresh_cache(self) -> Any:
|
||||
"""刷新缓存"""
|
||||
return await self.client.post(f"{self.base_path}/refresh")
|
||||
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
字典管理API封装
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class DictTypeAPI:
|
||||
"""字典类型API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/dict/types"
|
||||
|
||||
async def get_all(self) -> Any:
|
||||
"""获取所有字典类型"""
|
||||
return await self.client.get(self.base_path)
|
||||
|
||||
async def get_by_id(self, dict_id: int) -> Any:
|
||||
"""根据ID获取字典类型"""
|
||||
return await self.client.get(f"{self.base_path}/{dict_id}")
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建字典类型"""
|
||||
return await self.client.post(self.base_path, json=data)
|
||||
|
||||
async def update(self, dict_id: int, data: Dict[str, Any]) -> Any:
|
||||
"""更新字典类型"""
|
||||
return await self.client.put(f"{self.base_path}/{dict_id}", json=data)
|
||||
|
||||
async def delete(self, dict_id: int) -> Any:
|
||||
"""删除字典类型"""
|
||||
return await self.client.delete(f"{self.base_path}/{dict_id}")
|
||||
|
||||
|
||||
class DictDataAPI:
|
||||
"""字典数据API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/dict/data"
|
||||
|
||||
async def get_all(self) -> Any:
|
||||
"""获取所有字典数据"""
|
||||
return await self.client.get(self.base_path)
|
||||
|
||||
async def get_by_id(self, data_id: int) -> Any:
|
||||
"""根据ID获取字典数据"""
|
||||
return await self.client.get(f"{self.base_path}/{data_id}")
|
||||
|
||||
async def get_by_type(self, dict_type: str) -> Any:
|
||||
"""根据字典类型获取字典数据"""
|
||||
return await self.client.get(f"{self.base_path}/type/{dict_type}")
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建字典数据"""
|
||||
return await self.client.post(self.base_path, json=data)
|
||||
|
||||
async def update(self, data_id: int, data: Dict[str, Any]) -> Any:
|
||||
"""更新字典数据"""
|
||||
return await self.client.put(f"{self.base_path}/{data_id}", json=data)
|
||||
|
||||
async def delete(self, data_id: int) -> Any:
|
||||
"""删除字典数据"""
|
||||
return await self.client.delete(f"{self.base_path}/{data_id}")
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
字典管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class DictionaryAPI(BaseAPI):
|
||||
"""字典管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/dictionaries")
|
||||
|
||||
async def create_dictionary(self, dict_data: Dict[str, Any]) -> Response:
|
||||
"""创建字典"""
|
||||
return await self.post("", json=dict_data)
|
||||
|
||||
async def get_dictionary_by_id(self, dict_id: int) -> Response:
|
||||
"""根据ID获取字典"""
|
||||
return await self.get(f"/{dict_id}")
|
||||
|
||||
async def get_dictionaries_by_type(self, dict_type: str) -> Response:
|
||||
"""根据类型获取字典"""
|
||||
return await self.get(f"/type/{dict_type}")
|
||||
|
||||
async def get_all_dictionaries(self) -> Response:
|
||||
"""获取所有字典"""
|
||||
return await self.get("")
|
||||
|
||||
async def update_dictionary(self, dict_id: int, dict_data: Dict[str, Any]) -> Response:
|
||||
"""更新字典"""
|
||||
return await self.put(f"/{dict_id}", json=dict_data)
|
||||
|
||||
async def delete_dictionary(self, dict_id: int) -> Response:
|
||||
"""删除字典"""
|
||||
return await self.delete(f"/{dict_id}")
|
||||
|
||||
async def check_type_and_code_exists(self, dict_type: str, code: str) -> Response:
|
||||
"""检查类型和编码是否存在"""
|
||||
return await self.get("/check/exists", params={"type": dict_type, "code": code})
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
文件管理API封装
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class SysFileAPI:
|
||||
"""文件管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/files"
|
||||
|
||||
async def get_all(self) -> Any:
|
||||
"""获取所有文件"""
|
||||
return await self.client.get(self.base_path)
|
||||
|
||||
async def get_by_id(self, file_id: int) -> Any:
|
||||
"""根据ID获取文件信息"""
|
||||
return await self.client.get(f"{self.base_path}/{file_id}")
|
||||
|
||||
async def upload(self, file_path: str, create_by: str = "test") -> Any:
|
||||
"""上传文件"""
|
||||
with open(file_path, "rb") as f:
|
||||
files = {"file": f}
|
||||
data = {"createBy": create_by}
|
||||
return await self.client.post(f"{self.base_path}/upload", files=files, data=data)
|
||||
|
||||
async def download(self, file_name: str) -> Any:
|
||||
"""下载文件"""
|
||||
return await self.client.get(f"{self.base_path}/download/{file_name}")
|
||||
|
||||
async def preview(self, file_name: str) -> Any:
|
||||
"""预览文件"""
|
||||
return await self.client.get(f"{self.base_path}/preview/{file_name}")
|
||||
|
||||
async def delete(self, file_id: int) -> Any:
|
||||
"""删除文件"""
|
||||
return await self.client.delete(f"{self.base_path}/{file_id}")
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
菜单管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class MenuAPI(BaseAPI):
|
||||
"""菜单管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/menus")
|
||||
|
||||
async def create_menu(self, menu_data: Dict[str, Any]) -> Response:
|
||||
"""创建菜单"""
|
||||
return await self.post("", json=menu_data)
|
||||
|
||||
async def get_menu_by_id(self, menu_id: int) -> Response:
|
||||
"""根据ID获取菜单"""
|
||||
return await self.get(f"/{menu_id}")
|
||||
|
||||
async def get_all_menus(self) -> Response:
|
||||
"""获取所有菜单"""
|
||||
return await self.get("")
|
||||
|
||||
async def get_menu_tree(self) -> Response:
|
||||
"""获取菜单树"""
|
||||
return await self.get("/tree")
|
||||
|
||||
async def update_menu(self, menu_id: int, menu_data: Dict[str, Any]) -> Response:
|
||||
"""更新菜单"""
|
||||
return await self.put(f"/{menu_id}", json=menu_data)
|
||||
|
||||
async def delete_menu(self, menu_id: int) -> Response:
|
||||
"""删除菜单"""
|
||||
return await self.delete(f"/{menu_id}")
|
||||
|
||||
async def get_menus_by_parent(self, parent_id: int) -> Response:
|
||||
"""根据父菜单ID获取子菜单"""
|
||||
return await self.get("", params={"parentId": parent_id})
|
||||
|
||||
async def get_menus_by_type(self, menu_type: str) -> Response:
|
||||
"""根据菜单类型获取菜单"""
|
||||
return await self.get("", params={"menuType": menu_type})
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
通知公告API封装
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class SysNoticeAPI:
|
||||
"""系统公告API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/notices"
|
||||
|
||||
async def get_all(self) -> Any:
|
||||
"""获取所有公告"""
|
||||
return await self.client.get(self.base_path)
|
||||
|
||||
async def get_by_id(self, notice_id: int) -> Any:
|
||||
"""根据ID获取公告"""
|
||||
return await self.client.get(f"{self.base_path}/{notice_id}")
|
||||
|
||||
async def get_by_status(self, status: str) -> Any:
|
||||
"""根据状态获取公告"""
|
||||
return await self.client.get(f"{self.base_path}/status/{status}")
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建公告"""
|
||||
return await self.client.post(self.base_path, json=data)
|
||||
|
||||
async def update(self, notice_id: int, data: Dict[str, Any]) -> Any:
|
||||
"""更新公告"""
|
||||
return await self.client.put(f"{self.base_path}/{notice_id}", json=data)
|
||||
|
||||
async def delete(self, notice_id: int) -> Any:
|
||||
"""删除公告"""
|
||||
return await self.client.delete(f"{self.base_path}/{notice_id}")
|
||||
|
||||
|
||||
class SysMessageAPI:
|
||||
"""用户消息API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self.base_path = "/api/messages"
|
||||
|
||||
async def get_by_user(self, user_id: int) -> Any:
|
||||
"""获取用户所有消息"""
|
||||
return await self.client.get(f"{self.base_path}/user/{user_id}")
|
||||
|
||||
async def get_unread_count(self, user_id: int) -> Any:
|
||||
"""获取未读消息数量"""
|
||||
return await self.client.get(f"{self.base_path}/user/{user_id}/unread")
|
||||
|
||||
async def get_unread_list(self, user_id: int) -> Any:
|
||||
"""获取未读消息列表"""
|
||||
return await self.client.get(f"{self.base_path}/user/{user_id}/unread/list")
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> Any:
|
||||
"""创建消息"""
|
||||
return await self.client.post(self.base_path, json=data)
|
||||
|
||||
async def mark_as_read(self, message_id: int) -> Any:
|
||||
"""标记消息为已读"""
|
||||
return await self.client.put(f"{self.base_path}/{message_id}/read")
|
||||
|
||||
async def delete(self, message_id: int) -> Any:
|
||||
"""删除消息"""
|
||||
return await self.client.delete(f"{self.base_path}/{message_id}")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
角色管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class RoleAPI(BaseAPI):
|
||||
"""角色管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/roles")
|
||||
|
||||
async def create_role(self, role_data: Dict[str, Any]) -> Response:
|
||||
"""创建角色"""
|
||||
return await self.post("", json=role_data)
|
||||
|
||||
async def get_role_by_id(self, role_id: int) -> Response:
|
||||
"""根据ID获取角色"""
|
||||
return await self.get(f"/{role_id}")
|
||||
|
||||
async def get_role_by_name(self, role_name: str) -> Response:
|
||||
"""根据名称获取角色"""
|
||||
return await self.get(f"/name/{role_name}")
|
||||
|
||||
async def get_all_roles(self, include_deleted: bool = False) -> Response:
|
||||
"""获取所有角色"""
|
||||
return await self.get("", params={"includeDeleted": include_deleted})
|
||||
|
||||
async def update_role(self, role_id: int, role_data: Dict[str, Any]) -> Response:
|
||||
"""更新角色"""
|
||||
return await self.put(f"/{role_id}", json=role_data)
|
||||
|
||||
async def delete_role(self, role_id: int) -> Response:
|
||||
"""删除角色(逻辑删除)"""
|
||||
return await self.delete(f"/{role_id}")
|
||||
|
||||
async def restore_role(self, role_id: int) -> Response:
|
||||
"""恢复角色"""
|
||||
return await self.post(f"/{role_id}/restore")
|
||||
|
||||
async def check_name_exists(self, role_name: str) -> Response:
|
||||
"""检查角色名是否存在"""
|
||||
return await self.get("/check-name", params={"name": role_name})
|
||||
|
||||
async def get_roles_by_page(self, page: int = 0, size: int = 10,
|
||||
sort: str = "id", order: str = "asc",
|
||||
keyword: str = None) -> Response:
|
||||
"""分页获取角色"""
|
||||
params = {"page": page, "size": size, "sort": sort, "order": order}
|
||||
if keyword:
|
||||
params["keyword"] = keyword
|
||||
return await self.get("/page", params=params)
|
||||
|
||||
async def get_role_count(self) -> Response:
|
||||
"""获取角色总数"""
|
||||
return await self.get("/count")
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
用户管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class UserAPI(BaseAPI):
|
||||
"""用户管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/users")
|
||||
|
||||
async def create_user(self, user_data: Dict[str, Any]) -> Response:
|
||||
"""创建用户"""
|
||||
return await self.post("", json=user_data)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Response:
|
||||
"""根据ID获取用户"""
|
||||
return await self.get(f"/{user_id}")
|
||||
|
||||
async def get_all_users(self, include_deleted: bool = False) -> Response:
|
||||
"""获取所有用户"""
|
||||
return await self.get("", params={"includeDeleted": include_deleted})
|
||||
|
||||
async def update_user(self, user_id: int, user_data: Dict[str, Any]) -> Response:
|
||||
"""更新用户"""
|
||||
return await self.put(f"/{user_id}", json=user_data)
|
||||
|
||||
async def delete_user(self, user_id: int) -> Response:
|
||||
"""删除用户"""
|
||||
return await self.delete(f"/{user_id}")
|
||||
|
||||
async def logical_delete_user(self, user_id: int) -> Response:
|
||||
"""逻辑删除用户"""
|
||||
return await self.delete(f"/{user_id}/logical")
|
||||
|
||||
async def logical_delete_users(self, user_ids: List[int]) -> Response:
|
||||
"""批量逻辑删除用户"""
|
||||
return await self.post("/logical-delete", json=user_ids)
|
||||
|
||||
async def restore_user(self, user_id: int) -> Response:
|
||||
"""恢复用户"""
|
||||
return await self.post(f"/{user_id}/restore")
|
||||
|
||||
async def restore_users(self, user_ids: List[int]) -> Response:
|
||||
"""批量恢复用户"""
|
||||
return await self.post("/restore", json=user_ids)
|
||||
|
||||
async def check_username_exists(self, username: str) -> Response:
|
||||
"""检查用户名是否存在"""
|
||||
return await self.get("/check/username", params={"username": username})
|
||||
|
||||
async def check_email_exists(self, email: str) -> Response:
|
||||
"""检查邮箱是否存在"""
|
||||
return await self.get("/check/email", params={"email": email})
|
||||
|
||||
async def get_users_by_page(self, page: int = 0, size: int = 10,
|
||||
sort: str = "id", order: str = "asc",
|
||||
keyword: str = None) -> Response:
|
||||
"""分页获取用户"""
|
||||
params = {"page": page, "size": size, "sort": sort, "order": order}
|
||||
if keyword:
|
||||
params["keyword"] = keyword
|
||||
return await self.get("/page", params=params)
|
||||
|
||||
async def get_user_count(self) -> Response:
|
||||
"""获取用户总数"""
|
||||
return await self.get("/count")
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
断言工具
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from httpx import Response
|
||||
|
||||
|
||||
class Assertions:
|
||||
"""断言工具类"""
|
||||
|
||||
@staticmethod
|
||||
def assert_status_code(response: Response, expected_status: int):
|
||||
"""断言状态码"""
|
||||
assert response.status_code == expected_status, \
|
||||
f"Expected status code {expected_status}, got {response.status_code}. Response: {response.text}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_contains(response: Response, key: str, value: Any = None):
|
||||
"""断言响应包含指定字段"""
|
||||
data = response.json()
|
||||
assert key in data, f"Response does not contain key '{key}'. Response: {data}"
|
||||
if value is not None:
|
||||
assert data[key] == value, \
|
||||
f"Expected {value} for key '{key}', got {data[key]}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_is_list(response: Response):
|
||||
"""断言响应是列表"""
|
||||
data = response.json()
|
||||
assert isinstance(data, list), f"Expected list, got {type(data)}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_not_empty(response: Response):
|
||||
"""断言响应不为空"""
|
||||
data = response.json()
|
||||
assert data, f"Response is empty. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_type(response: Response, field: str, expected_type: type):
|
||||
"""断言响应字段类型"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
assert isinstance(data[field], expected_type), \
|
||||
f"Expected field '{field}' to be {expected_type}, got {type(data[field])}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_fields_present(response: Response, fields: List[str]):
|
||||
"""断言响应包含所有指定字段"""
|
||||
data = response.json()
|
||||
missing_fields = [field for field in fields if field not in data]
|
||||
assert not missing_fields, \
|
||||
f"Response is missing fields: {missing_fields}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_length(response: Response, field: str, min_length: int = None, max_length: int = None):
|
||||
"""断言响应字段长度"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
field_value = data[field]
|
||||
|
||||
if isinstance(field_value, (str, list, dict)):
|
||||
length = len(field_value)
|
||||
if min_length is not None:
|
||||
assert length >= min_length, \
|
||||
f"Field '{field}' length {length} is less than minimum {min_length}"
|
||||
if max_length is not None:
|
||||
assert length <= max_length, \
|
||||
f"Field '{field}' length {length} is greater than maximum {max_length}"
|
||||
else:
|
||||
raise AssertionError(f"Field '{field}' is not a string, list, or dict")
|
||||
|
||||
@staticmethod
|
||||
def assert_error_response(response: Response, expected_message: str = None):
|
||||
"""断言错误响应"""
|
||||
Assertions.assert_status_code(response, 400)
|
||||
if expected_message:
|
||||
data = response.json()
|
||||
assert expected_message in str(data), \
|
||||
f"Expected error message '{expected_message}' not found in response: {data}"
|
||||
|
||||
|
||||
assertions = Assertions()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
测试数据生成器
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from faker import Faker
|
||||
|
||||
|
||||
class DataGenerator:
|
||||
"""测试数据生成器"""
|
||||
|
||||
def __init__(self, locale: str = "zh_CN"):
|
||||
self.faker = Faker(locale)
|
||||
|
||||
def generate_username(self) -> str:
|
||||
"""生成用户名"""
|
||||
return f"testuser_{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}"
|
||||
|
||||
def generate_password(self, length: int = 12) -> str:
|
||||
"""生成密码"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(random.choices(chars, k=length))
|
||||
|
||||
def generate_email(self) -> str:
|
||||
"""生成邮箱"""
|
||||
return self.faker.email()
|
||||
|
||||
def generate_phone(self) -> str:
|
||||
"""生成手机号"""
|
||||
return self.faker.phone_number()
|
||||
|
||||
def generate_name(self) -> str:
|
||||
"""生成姓名"""
|
||||
return self.faker.name()
|
||||
|
||||
def generate_role_name(self) -> str:
|
||||
"""生成角色名"""
|
||||
return f"ROLE_{''.join(random.choices(string.ascii_uppercase, k=6))}"
|
||||
|
||||
def generate_dict_type(self) -> str:
|
||||
"""生成字典类型"""
|
||||
return f"DICT_TYPE_{''.join(random.choices(string.ascii_uppercase, k=4))}"
|
||||
|
||||
def generate_dict_code(self) -> str:
|
||||
"""生成字典编码"""
|
||||
return f"CODE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=6))}"
|
||||
|
||||
def generate_url(self) -> str:
|
||||
"""生成URL"""
|
||||
return self.faker.url()
|
||||
|
||||
def generate_company_name(self) -> str:
|
||||
"""生成公司名"""
|
||||
return self.faker.company()
|
||||
|
||||
def generate_address(self) -> str:
|
||||
"""生成地址"""
|
||||
return self.faker.address()
|
||||
|
||||
def generate_description(self) -> str:
|
||||
"""生成描述"""
|
||||
return self.faker.text(max_nb_chars=200)
|
||||
|
||||
def generate_permissions(self) -> str:
|
||||
"""生成权限字符串"""
|
||||
permissions = ["READ", "WRITE", "DELETE", "ADMIN", "MANAGE"]
|
||||
selected = random.sample(permissions, random.randint(1, len(permissions)))
|
||||
return ",".join(selected)
|
||||
|
||||
|
||||
data_generator = DataGenerator()
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
测试数据管理工具(简化版)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Callable
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TestDataManager:
|
||||
"""测试数据管理器"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self._users: List[int] = []
|
||||
self._roles: List[int] = []
|
||||
self._menus: List[int] = []
|
||||
self._dictionaries: List[int] = []
|
||||
self._dict_types: List[int] = []
|
||||
self._configs: List[int] = []
|
||||
self._notices: List[int] = []
|
||||
self._files: List[int] = []
|
||||
self._messages: List[int] = []
|
||||
|
||||
def add_user(self, user_id: int):
|
||||
"""添加用户到清理列表"""
|
||||
self._users.append(user_id)
|
||||
|
||||
def add_role(self, role_id: int):
|
||||
"""添加角色到清理列表"""
|
||||
self._roles.append(role_id)
|
||||
|
||||
def add_menu(self, menu_id: int):
|
||||
"""添加菜单到清理列表"""
|
||||
self._menus.append(menu_id)
|
||||
|
||||
def add_dictionary(self, dict_id: int):
|
||||
"""添加字典到清理列表"""
|
||||
self._dictionaries.append(dict_id)
|
||||
|
||||
def add_dict_type(self, dict_type_id: int):
|
||||
"""添加字典类型到清理列表"""
|
||||
self._dict_types.append(dict_type_id)
|
||||
|
||||
def add_config(self, config_id: int):
|
||||
"""添加系统配置到清理列表"""
|
||||
self._configs.append(config_id)
|
||||
|
||||
def add_notice(self, notice_id: int):
|
||||
"""添加系统公告到清理列表"""
|
||||
self._notices.append(notice_id)
|
||||
|
||||
def add_file(self, file_id: int):
|
||||
"""添加文件到清理列表"""
|
||||
self._files.append(file_id)
|
||||
|
||||
def add_message(self, message_id: int):
|
||||
"""添加消息到清理列表"""
|
||||
self._messages.append(message_id)
|
||||
|
||||
async def cleanup_all(self):
|
||||
"""清理所有测试数据"""
|
||||
logger.info("Starting test data cleanup...")
|
||||
|
||||
cleanup_tasks = []
|
||||
|
||||
if self._messages:
|
||||
cleanup_tasks.extend([self._delete_message(msg_id) for msg_id in self._messages])
|
||||
self._messages.clear()
|
||||
|
||||
if self._files:
|
||||
cleanup_tasks.extend([self._delete_file(file_id) for file_id in self._files])
|
||||
self._files.clear()
|
||||
|
||||
if self._notices:
|
||||
cleanup_tasks.extend([self._delete_notice(notice_id) for notice_id in self._notices])
|
||||
self._notices.clear()
|
||||
|
||||
if self._configs:
|
||||
cleanup_tasks.extend([self._delete_config(config_id) for config_id in self._configs])
|
||||
self._configs.clear()
|
||||
|
||||
if self._dictionaries:
|
||||
cleanup_tasks.extend([self._delete_dictionary(dict_id) for dict_id in self._dictionaries])
|
||||
self._dictionaries.clear()
|
||||
|
||||
if self._dict_types:
|
||||
cleanup_tasks.extend([self._delete_dict_type(dict_type_id) for dict_type_id in self._dict_types])
|
||||
self._dict_types.clear()
|
||||
|
||||
if self._users:
|
||||
cleanup_tasks.extend([self._delete_user(user_id) for user_id in self._users])
|
||||
self._users.clear()
|
||||
|
||||
if self._roles:
|
||||
cleanup_tasks.extend([self._delete_role(role_id) for role_id in self._roles])
|
||||
self._roles.clear()
|
||||
|
||||
if self._menus:
|
||||
cleanup_tasks.extend([self._delete_menu(menu_id) for menu_id in self._menus])
|
||||
self._menus.clear()
|
||||
|
||||
if cleanup_tasks:
|
||||
results = await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
||||
failed_count = sum(1 for r in results if isinstance(r, Exception))
|
||||
if failed_count > 0:
|
||||
logger.warning(f"Failed to cleanup {failed_count} resources")
|
||||
|
||||
logger.info("Test data cleanup completed")
|
||||
|
||||
async def _delete_user(self, user_id: int):
|
||||
"""删除用户"""
|
||||
try:
|
||||
await self.client.delete(f"/api/users/{user_id}")
|
||||
logger.info(f"Cleaned up user {user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup user {user_id}: {e}")
|
||||
|
||||
async def _delete_role(self, role_id: int):
|
||||
"""删除角色"""
|
||||
try:
|
||||
await self.client.delete(f"/api/roles/{role_id}")
|
||||
logger.info(f"Cleaned up role {role_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup role {role_id}: {e}")
|
||||
|
||||
async def _delete_menu(self, menu_id: int):
|
||||
"""删除菜单"""
|
||||
try:
|
||||
await self.client.delete(f"/api/menus/{menu_id}")
|
||||
logger.info(f"Cleaned up menu {menu_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup menu {menu_id}: {e}")
|
||||
|
||||
async def _delete_dictionary(self, dict_id: int):
|
||||
"""删除字典"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dictionaries/{dict_id}")
|
||||
logger.info(f"Cleaned up dictionary {dict_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dictionary {dict_id}: {e}")
|
||||
|
||||
async def _delete_dict_type(self, dict_type_id: int):
|
||||
"""删除字典类型"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dict/types/{dict_type_id}")
|
||||
logger.info(f"Cleaned up dict type {dict_type_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dict type {dict_type_id}: {e}")
|
||||
|
||||
async def _delete_config(self, config_id: int):
|
||||
"""删除系统配置"""
|
||||
try:
|
||||
await self.client.delete(f"/api/config/{config_id}")
|
||||
logger.info(f"Cleaned up config {config_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup config {config_id}: {e}")
|
||||
|
||||
async def _delete_notice(self, notice_id: int):
|
||||
"""删除系统公告"""
|
||||
try:
|
||||
await self.client.delete(f"/api/notices/{notice_id}")
|
||||
logger.info(f"Cleaned up notice {notice_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup notice {notice_id}: {e}")
|
||||
|
||||
async def _delete_file(self, file_id: int):
|
||||
"""删除文件"""
|
||||
try:
|
||||
await self.client.delete(f"/api/files/{file_id}")
|
||||
logger.info(f"Cleaned up file {file_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup file {file_id}: {e}")
|
||||
|
||||
async def _delete_message(self, message_id: int):
|
||||
"""删除消息"""
|
||||
try:
|
||||
await self.client.delete(f"/api/messages/{message_id}")
|
||||
logger.info(f"Cleaned up message {message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup message {message_id}: {e}")
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
"users": len(self._users),
|
||||
"roles": len(self._roles),
|
||||
"menus": len(self._menus),
|
||||
"dictionaries": len(self._dictionaries),
|
||||
"dict_types": len(self._dict_types),
|
||||
"configs": len(self._configs),
|
||||
"notices": len(self._notices),
|
||||
"files": len(self._files),
|
||||
"messages": len(self._messages)
|
||||
}
|
||||
|
||||
def has_data(self) -> bool:
|
||||
"""检查是否有待清理数据"""
|
||||
return any([
|
||||
self._users, self._roles, self._menus,
|
||||
self._dictionaries, self._dict_types, self._configs,
|
||||
self._notices, self._files, self._messages
|
||||
])
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
日志工具
|
||||
"""
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_logger(log_file: str = "e2e_tests.log", log_level: str = "INFO"):
|
||||
"""配置日志"""
|
||||
logger.remove()
|
||||
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level=log_level,
|
||||
colorize=True
|
||||
)
|
||||
|
||||
logger.add(
|
||||
log_file,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
level=log_level,
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
setup_logger()
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
测试数据管理工具(简化版)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Callable
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TestDataManager:
|
||||
"""测试数据管理器"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self._users: List[int] = []
|
||||
self._roles: List[int] = []
|
||||
self._menus: List[int] = []
|
||||
self._dictionaries: List[int] = []
|
||||
self._dict_types: List[int] = []
|
||||
self._configs: List[int] = []
|
||||
self._notices: List[int] = []
|
||||
self._files: List[int] = []
|
||||
self._messages: List[int] = []
|
||||
|
||||
def add_user(self, user_id: int):
|
||||
"""添加用户到清理列表"""
|
||||
self._users.append(user_id)
|
||||
|
||||
def add_role(self, role_id: int):
|
||||
"""添加角色到清理列表"""
|
||||
self._roles.append(role_id)
|
||||
|
||||
def add_menu(self, menu_id: int):
|
||||
"""添加菜单到清理列表"""
|
||||
self._menus.append(menu_id)
|
||||
|
||||
def add_dictionary(self, dict_id: int):
|
||||
"""添加字典到清理列表"""
|
||||
self._dictionaries.append(dict_id)
|
||||
|
||||
def add_dict_type(self, dict_type_id: int):
|
||||
"""添加字典类型到清理列表"""
|
||||
self._dict_types.append(dict_type_id)
|
||||
|
||||
def add_config(self, config_id: int):
|
||||
"""添加系统配置到清理列表"""
|
||||
self._configs.append(config_id)
|
||||
|
||||
def add_notice(self, notice_id: int):
|
||||
"""添加系统公告到清理列表"""
|
||||
self._notices.append(notice_id)
|
||||
|
||||
def add_file(self, file_id: int):
|
||||
"""添加文件到清理列表"""
|
||||
self._files.append(file_id)
|
||||
|
||||
def add_message(self, message_id: int):
|
||||
"""添加消息到清理列表"""
|
||||
self._messages.append(message_id)
|
||||
|
||||
async def cleanup_all(self):
|
||||
"""清理所有测试数据"""
|
||||
logger.info("Starting test data cleanup...")
|
||||
|
||||
cleanup_tasks = []
|
||||
|
||||
if self._messages:
|
||||
cleanup_tasks.extend([self._delete_message(msg_id) for msg_id in self._messages])
|
||||
self._messages.clear()
|
||||
|
||||
if self._files:
|
||||
cleanup_tasks.extend([self._delete_file(file_id) for file_id in self._files])
|
||||
self._files.clear()
|
||||
|
||||
if self._notices:
|
||||
cleanup_tasks.extend([self._delete_notice(notice_id) for notice_id in self._notices])
|
||||
self._notices.clear()
|
||||
|
||||
if self._configs:
|
||||
cleanup_tasks.extend([self._delete_config(config_id) for config_id in self._configs])
|
||||
self._configs.clear()
|
||||
|
||||
if self._dictionaries:
|
||||
cleanup_tasks.extend([self._delete_dictionary(dict_id) for dict_id in self._dictionaries])
|
||||
self._dictionaries.clear()
|
||||
|
||||
if self._dict_types:
|
||||
cleanup_tasks.extend([self._delete_dict_type(dict_type_id) for dict_type_id in self._dict_types])
|
||||
self._dict_types.clear()
|
||||
|
||||
if self._users:
|
||||
cleanup_tasks.extend([self._delete_user(user_id) for user_id in self._users])
|
||||
self._users.clear()
|
||||
|
||||
if self._roles:
|
||||
cleanup_tasks.extend([self._delete_role(role_id) for role_id in self._roles])
|
||||
self._roles.clear()
|
||||
|
||||
if self._menus:
|
||||
cleanup_tasks.extend([self._delete_menu(menu_id) for menu_id in self._menus])
|
||||
self._menus.clear()
|
||||
|
||||
if cleanup_tasks:
|
||||
results = await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
||||
failed_count = sum(1 for r in results if isinstance(r, Exception))
|
||||
if failed_count > 0:
|
||||
logger.warning(f"Failed to cleanup {failed_count} resources")
|
||||
|
||||
logger.info("Test data cleanup completed")
|
||||
|
||||
async def _delete_user(self, user_id: int):
|
||||
"""删除用户"""
|
||||
try:
|
||||
await self.client.delete(f"/api/users/{user_id}")
|
||||
logger.info(f"Cleaned up user {user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup user {user_id}: {e}")
|
||||
|
||||
async def _delete_role(self, role_id: int):
|
||||
"""删除角色"""
|
||||
try:
|
||||
await self.client.delete(f"/api/roles/{role_id}")
|
||||
logger.info(f"Cleaned up role {role_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup role {role_id}: {e}")
|
||||
|
||||
async def _delete_menu(self, menu_id: int):
|
||||
"""删除菜单"""
|
||||
try:
|
||||
await self.client.delete(f"/api/menus/{menu_id}")
|
||||
logger.info(f"Cleaned up menu {menu_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup menu {menu_id}: {e}")
|
||||
|
||||
async def _delete_dictionary(self, dict_id: int):
|
||||
"""删除字典"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dictionaries/{dict_id}")
|
||||
logger.info(f"Cleaned up dictionary {dict_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dictionary {dict_id}: {e}")
|
||||
|
||||
async def _delete_dict_type(self, dict_type_id: int):
|
||||
"""删除字典类型"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dict/types/{dict_type_id}")
|
||||
logger.info(f"Cleaned up dict type {dict_type_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dict type {dict_type_id}: {e}")
|
||||
|
||||
async def _delete_config(self, config_id: int):
|
||||
"""删除系统配置"""
|
||||
try:
|
||||
await self.client.delete(f"/api/config/{config_id}")
|
||||
logger.info(f"Cleaned up config {config_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup config {config_id}: {e}")
|
||||
|
||||
async def _delete_notice(self, notice_id: int):
|
||||
"""删除系统公告"""
|
||||
try:
|
||||
await self.client.delete(f"/api/notices/{notice_id}")
|
||||
logger.info(f"Cleaned up notice {notice_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup notice {notice_id}: {e}")
|
||||
|
||||
async def _delete_file(self, file_id: int):
|
||||
"""删除文件"""
|
||||
try:
|
||||
await self.client.delete(f"/api/files/{file_id}")
|
||||
logger.info(f"Cleaned up file {file_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup file {file_id}: {e}")
|
||||
|
||||
async def _delete_message(self, message_id: int):
|
||||
"""删除消息"""
|
||||
try:
|
||||
await self.client.delete(f"/api/messages/{message_id}")
|
||||
logger.info(f"Cleaned up message {message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup message {message_id}: {e}")
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
"users": len(self._users),
|
||||
"roles": len(self._roles),
|
||||
"menus": len(self._menus),
|
||||
"dictionaries": len(self._dictionaries),
|
||||
"dict_types": len(self._dict_types),
|
||||
"configs": len(self._configs),
|
||||
"notices": len(self._notices),
|
||||
"files": len(self._files),
|
||||
"messages": len(self._messages)
|
||||
}
|
||||
|
||||
def has_data(self) -> bool:
|
||||
"""检查是否有待清理数据"""
|
||||
return any([
|
||||
self._users, self._roles, self._menus,
|
||||
self._dictionaries, self._dict_types, self._configs,
|
||||
self._notices, self._files, self._messages
|
||||
])
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
端到端业务流程测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.auth_api import AuthAPI
|
||||
from test_utils.api_client.api.user_api import UserAPI
|
||||
from test_utils.api_client.api.role_api import RoleAPI
|
||||
from test_utils.api_client.api.notice_api import SysNoticeAPI
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.regression
|
||||
class TestBusinessFlow:
|
||||
"""端到端业务流程测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_user_lifecycle(self, authenticated_client):
|
||||
"""测试完整用户生命周期"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
new_user_data = {
|
||||
"username": f"e2e_user_{timestamp}",
|
||||
"password": "Test123!@#",
|
||||
"email": f"e2e_{timestamp}@example.com",
|
||||
"phone": "13800138000",
|
||||
"status": 1
|
||||
}
|
||||
|
||||
create_response = await user_api.create_user(new_user_data)
|
||||
assert create_response.status_code == 201
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 200
|
||||
user_data = get_response.json()
|
||||
assert user_data["username"] == new_user_data["username"]
|
||||
|
||||
update_data = {"email": f"updated_{timestamp}@example.com"}
|
||||
update_response = await user_api.update_user(user_id, update_data)
|
||||
assert update_response.status_code == 200
|
||||
|
||||
delete_response = await user_api.delete_user(user_id)
|
||||
assert delete_response.status_code in [200, 204]
|
||||
|
||||
final_get_response = await user_api.get_user_by_id(user_id)
|
||||
assert final_get_response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_assignment_workflow(self, authenticated_client):
|
||||
"""测试角色分配工作流"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
role_data = {
|
||||
"roleName": f"E2E_Role_{timestamp}",
|
||||
"roleKey": f"e2e_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
role_response = await role_api.create_role(role_data)
|
||||
assert role_response.status_code == 201
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_data = {
|
||||
"username": f"e2e_user_{timestamp}",
|
||||
"password": "Test123!@#",
|
||||
"email": f"e2e_{timestamp}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
|
||||
user_response = await user_api.create_user(user_data)
|
||||
assert user_response.status_code == 201
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
assign_response = await user_api.update_user(user_id, {"roleId": role_id})
|
||||
assert assign_response.status_code == 200
|
||||
|
||||
verify_response = await user_api.get_user_by_id(user_id)
|
||||
assert verify_response.json()["roleId"] == role_id
|
||||
|
||||
await user_api.delete_user(user_id)
|
||||
await role_api.delete_role(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification_workflow(self, authenticated_client):
|
||||
"""测试通知工作流"""
|
||||
notice_api = SysNoticeAPI(authenticated_client)
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
notice_data = {
|
||||
"noticeTitle": f"E2E_Notice_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "This is an E2E test notice",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
create_response = await notice_api.create(notice_data)
|
||||
assert create_response.status_code == 201
|
||||
notice_data_response = create_response.json()
|
||||
|
||||
notice_id = notice_data_response.get("id")
|
||||
if not notice_id:
|
||||
notice_title = notice_data_response.get("noticeTitle")
|
||||
all_notices = await notice_api.get_all()
|
||||
notices = all_notices.json()
|
||||
notice = next((n for n in notices if n["noticeTitle"] == notice_title), None)
|
||||
notice_id = notice["id"] if notice else None
|
||||
|
||||
assert notice_id is not None
|
||||
|
||||
get_response = await notice_api.get_by_id(notice_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
all_notices = await notice_api.get_all()
|
||||
assert all_notices.status_code == 200
|
||||
notices = all_notices.json()
|
||||
assert any(notice["id"] == notice_id for notice in notices)
|
||||
|
||||
update_data = {"noticeTitle": f"Updated_Notice_{timestamp}"}
|
||||
update_response = await notice_api.update(notice_id, update_data)
|
||||
assert update_response.status_code == 200
|
||||
|
||||
await notice_api.delete(notice_id)
|
||||
|
||||
final_get = await notice_api.get_by_id(notice_id)
|
||||
assert final_get.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_role_user_management(self, authenticated_client):
|
||||
"""测试多角色用户管理"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
admin_role_data = {
|
||||
"roleName": f"Admin_{timestamp}",
|
||||
"roleKey": f"admin_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
admin_role = await role_api.create_role(admin_role_data)
|
||||
admin_role_id = admin_role.json()["id"]
|
||||
|
||||
user_role_data = {
|
||||
"roleName": f"User_{timestamp}",
|
||||
"roleKey": f"user_{timestamp}",
|
||||
"roleSort": 2,
|
||||
"status": 1
|
||||
}
|
||||
user_role = await role_api.create_role(user_role_data)
|
||||
user_role_id = user_role.json()["id"]
|
||||
|
||||
admin_user_data = {
|
||||
"username": f"admin_{timestamp}",
|
||||
"password": "Admin123!@#",
|
||||
"email": f"admin_{timestamp}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
admin_user = await user_api.create_user(admin_user_data)
|
||||
admin_user_id = admin_user.json()["id"]
|
||||
|
||||
regular_user_data = {
|
||||
"username": f"regular_{timestamp}",
|
||||
"password": "User123!@#",
|
||||
"email": f"regular_{timestamp}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
regular_user = await user_api.create_user(regular_user_data)
|
||||
regular_user_id = regular_user.json()["id"]
|
||||
|
||||
await user_api.update_user(admin_user_id, {"roleId": admin_role_id})
|
||||
await user_api.update_user(regular_user_id, {"roleId": user_role_id})
|
||||
|
||||
admin_verify = await user_api.get_user_by_id(admin_user_id)
|
||||
assert admin_verify.json()["roleId"] == admin_role_id
|
||||
|
||||
regular_verify = await user_api.get_user_by_id(regular_user_id)
|
||||
assert regular_verify.json()["roleId"] == user_role_id
|
||||
|
||||
all_users = await user_api.get_all_users()
|
||||
users = all_users.json()
|
||||
assert len(users) >= 2
|
||||
|
||||
await user_api.delete_user(admin_user_id)
|
||||
await user_api.delete_user(regular_user_id)
|
||||
await role_api.delete_role(admin_role_id)
|
||||
await role_api.delete_role(user_role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_role_cascade_operations(self, authenticated_client):
|
||||
"""测试用户角色级联操作"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
role_data = {
|
||||
"roleName": f"Cascade_Role_{timestamp}",
|
||||
"roleKey": f"cascade_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
role_response = await role_api.create_role(role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_ids = []
|
||||
for i in range(3):
|
||||
user_data = {
|
||||
"username": f"cascade_user_{timestamp}_{i}",
|
||||
"password": "Test123!@#",
|
||||
"email": f"cascade_{timestamp}_{i}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
user_response = await user_api.create_user(user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
user_ids.append(user_id)
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
await role_api.update_role(role_id, {"status": 0})
|
||||
|
||||
for user_id in user_ids:
|
||||
user_data = await user_api.get_user_by_id(user_id)
|
||||
assert user_data.json()["roleId"] == role_id
|
||||
|
||||
for user_id in user_ids:
|
||||
await user_api.delete_user(user_id)
|
||||
await role_api.delete_role(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_and_filter_workflow(self, authenticated_client):
|
||||
"""测试搜索和过滤工作流"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
role_data = {
|
||||
"roleName": f"Search_Role_{timestamp}",
|
||||
"roleKey": f"search_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
role_response = await role_api.create_role(role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_ids = []
|
||||
for i in range(5):
|
||||
user_data = {
|
||||
"username": f"search_{timestamp}_{i}",
|
||||
"password": "Test123!@#",
|
||||
"email": f"search_{timestamp}_{i}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
user_response = await user_api.create_user(user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
user_ids.append(user_id)
|
||||
|
||||
search_response = await user_api.get_users_by_page(keyword=f"search_{timestamp}")
|
||||
assert search_response.status_code == 200
|
||||
search_data = search_response.json()
|
||||
assert len(search_data["content"]) >= 5
|
||||
|
||||
all_users = await user_api.get_all_users()
|
||||
assert all_users.status_code == 200
|
||||
|
||||
for user_id in user_ids:
|
||||
await user_api.delete_user(user_id)
|
||||
await role_api.delete_role(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_recovery_workflow(self, authenticated_client):
|
||||
"""测试错误恢复工作流"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
invalid_user_data = {
|
||||
"username": "",
|
||||
"password": "123",
|
||||
"email": "invalid-email"
|
||||
}
|
||||
|
||||
invalid_response = await user_api.create_user(invalid_user_data)
|
||||
assert invalid_response.status_code in [400, 409, 422]
|
||||
|
||||
valid_user_data = {
|
||||
"username": f"recovery_{timestamp}",
|
||||
"password": "Valid123!@#",
|
||||
"email": f"recovery_{timestamp}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
|
||||
valid_response = await user_api.create_user(valid_user_data)
|
||||
assert valid_response.status_code == 201
|
||||
user_id = valid_response.json()["id"]
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
await user_api.delete_user(user_id)
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('用户认证 E2E 测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
});
|
||||
|
||||
test('成功登录流程', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(/登录/);
|
||||
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[type="password"]', 'admin123');
|
||||
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
await page.waitForURL('**/dashboard');
|
||||
await expect(page.locator('.user-info')).toContainText('admin');
|
||||
});
|
||||
|
||||
test('登录失败 - 无效凭证', async ({ page }) => {
|
||||
await page.fill('input[placeholder*="用户名"]', 'invalid');
|
||||
await page.fill('input[type="password"]', 'invalid');
|
||||
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
await expect(page.locator('.error-message')).toBeVisible();
|
||||
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
|
||||
});
|
||||
|
||||
test('登录失败 - 缺少必填字段', async ({ page }) => {
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.error-message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('登出流程', async ({ page }) => {
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL('**/');
|
||||
|
||||
await page.click('text=登出');
|
||||
|
||||
await page.waitForURL('**/login');
|
||||
await expect(page).toHaveTitle(/登录/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('系统基础功能 E2E 测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('首页加载测试', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(/Novalon 管理系统/);
|
||||
await expect(page.locator('#app')).toBeVisible();
|
||||
});
|
||||
|
||||
test('登录页面访问测试', async ({ page }) => {
|
||||
await page.click('text=登录');
|
||||
await expect(page).toHaveURL(/.*login/);
|
||||
await expect(page.locator('input[type="text"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('后端健康检查', async ({ request }) => {
|
||||
const response = await request.get('http://localhost:8084/actuator/health');
|
||||
expect(response.status()).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.status).toBe('UP');
|
||||
});
|
||||
|
||||
test('数据库连接检查', async ({ request }) => {
|
||||
const response = await request.get('http://localhost:8084/actuator/health');
|
||||
expect(response.status()).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.components.r2dbc.status).toBe('UP');
|
||||
expect(body.components.r2dbc.details.database).toBe('PostgreSQL');
|
||||
});
|
||||
|
||||
test('前端页面可访问性', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#app')).toBeVisible();
|
||||
const title = await page.title();
|
||||
expect(title).toContain('Novalon 管理系统');
|
||||
});
|
||||
|
||||
test('API代理配置验证', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const response = await page.request.get('http://localhost:3002/api/actuator/health');
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('角色管理 E2E 测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[type="password"]', 'admin123');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForURL('**/dashboard');
|
||||
});
|
||||
|
||||
test('创建角色完整流程', async ({ page }) => {
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles');
|
||||
|
||||
await page.click('text=创建角色');
|
||||
|
||||
const timestamp = Date.now();
|
||||
const roleName = `测试角色_${timestamp}`;
|
||||
const roleKey = `test_role_${timestamp}`;
|
||||
|
||||
await page.fill('input[name="roleName"]', roleName);
|
||||
await page.fill('input[name="roleKey"]', roleKey);
|
||||
await page.fill('input[name="roleSort"]', '1');
|
||||
|
||||
await page.click('input[type="checkbox"][value="user:view"]');
|
||||
await page.click('input[type="checkbox"][value="user:create"]');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
await expect(page.locator('table')).toContainText(roleName);
|
||||
});
|
||||
|
||||
test('编辑角色流程', async ({ page }) => {
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles');
|
||||
|
||||
await page.click('table tbody tr:first-child .edit-button');
|
||||
|
||||
await page.fill('input[name="roleName"]', '更新后的角色名称');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
await expect(page.locator('table')).toContainText('更新后的角色名称');
|
||||
});
|
||||
|
||||
test('分配权限流程', async ({ page }) => {
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles');
|
||||
|
||||
await page.click('table tbody tr:first-child .permission-button');
|
||||
|
||||
await page.click('input[type="checkbox"][value="user:edit"]');
|
||||
await page.click('input[type="checkbox"][value="user:delete"]');
|
||||
|
||||
await page.click('.permission-dialog .save-button');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('删除角色流程', async ({ page }) => {
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles');
|
||||
|
||||
const firstRow = page.locator('table tbody tr:first-child');
|
||||
const roleName = await firstRow.locator('td:first-child').textContent();
|
||||
|
||||
await firstRow.locator('.delete-button').click();
|
||||
|
||||
await page.click('.confirm-dialog .confirm-button');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.locator('table')).not.toContainText(roleName);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('系统配置 E2E 测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[type="password"]', 'admin123');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForURL('**/dashboard');
|
||||
});
|
||||
|
||||
test('查看系统配置', async ({ page }) => {
|
||||
await page.click('text=系统配置');
|
||||
await page.waitForURL('**/config');
|
||||
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
await expect(page.locator('table tbody tr')).toHaveCount(10);
|
||||
});
|
||||
|
||||
test('编辑系统配置', async ({ page }) => {
|
||||
await page.click('text=系统配置');
|
||||
await page.waitForURL('**/config');
|
||||
|
||||
await page.click('table tbody tr:first-child .edit-button');
|
||||
|
||||
await page.fill('input[name="configValue"]', 'test_value_123');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('搜索配置项', async ({ page }) => {
|
||||
await page.click('text=系统配置');
|
||||
await page.waitForURL('**/config');
|
||||
|
||||
await page.fill('input[name="keyword"]', 'system');
|
||||
await page.click('button[type="search"]');
|
||||
|
||||
await expect(page.locator('table')).toContainText('system');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
真实的端到端(E2E)测试 - 使用Playwright测试前后端联通
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from playwright.async_api import async_playwright, Page, Browser, BrowserContext
|
||||
from httpx import AsyncClient
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.playwright
|
||||
class TestRealE2E:
|
||||
"""真实的端到端测试类"""
|
||||
|
||||
@pytest.fixture
|
||||
async def browser(self):
|
||||
"""浏览器fixture - headless模式"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
yield browser
|
||||
await browser.close()
|
||||
|
||||
@pytest.fixture
|
||||
async def context(self, browser):
|
||||
"""浏览器上下文fixture"""
|
||||
context = await browser.new_context()
|
||||
yield context
|
||||
await context.close()
|
||||
|
||||
@pytest.fixture
|
||||
async def page(self, context):
|
||||
"""页面fixture"""
|
||||
page = await context.new_page()
|
||||
page.set_default_timeout(30000)
|
||||
yield page
|
||||
await page.close()
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client(self):
|
||||
"""已认证的HTTP客户端"""
|
||||
async with AsyncClient(base_url=settings.API_BASE_URL) as client:
|
||||
response = await client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"username": settings.TEST_USERNAME,
|
||||
"password": settings.TEST_PASSWORD
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
token = response.json().get("token")
|
||||
client.headers.update({"Authorization": f"Bearer {token}"})
|
||||
yield client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_user_lifecycle_e2e(self, page, authenticated_client):
|
||||
"""测试完整的用户生命周期 - 前后端联通"""
|
||||
timestamp = int(time.time() * 1000)
|
||||
username = f"e2e_user_{timestamp}"
|
||||
email = f"e2e_{timestamp}@example.com"
|
||||
|
||||
# 1. 通过前端登录
|
||||
await page.goto("http://localhost:3002/login")
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
await page.fill('input[name="username"]', settings.TEST_USERNAME)
|
||||
await page.fill('input[name="password"]', settings.TEST_PASSWORD)
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
await page.wait_for_url("**/")
|
||||
assert await page.title() != ""
|
||||
|
||||
# 2. 通过前端创建用户
|
||||
await page.click('text=用户管理')
|
||||
await page.wait_for_url("**/users")
|
||||
|
||||
await page.click('text=创建用户')
|
||||
|
||||
await page.fill('input[name="username"]', username)
|
||||
await page.fill('input[name="email"]', email)
|
||||
await page.fill('input[name="phone"]', '13800138000')
|
||||
await page.fill('input[name="password"]', 'Test123!@#')
|
||||
await page.fill('input[name="confirmPassword"]', 'Test123!@#')
|
||||
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
await page.wait_for_selector('.success-message', timeout=10000)
|
||||
success_message = await page.text_content('.success-message')
|
||||
assert '成功' in success_message or 'success' in success_message.lower()
|
||||
|
||||
# 3. 通过API验证用户已创建
|
||||
response = await authenticated_client.get("/api/users")
|
||||
assert response.status_code == 200
|
||||
users = response.json()
|
||||
user_exists = any(user['username'] == username for user in users)
|
||||
assert user_exists, f"User {username} not found in API response"
|
||||
|
||||
# 4. 通过前端验证用户显示
|
||||
await page.reload()
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
page_content = await page.content()
|
||||
assert username in page_content, f"Username {username} not found in page content"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_assignment_e2e(self, page, authenticated_client):
|
||||
"""测试角色分配 - 前后端联通"""
|
||||
timestamp = int(time.time() * 1000)
|
||||
role_name = f"E2E_Role_{timestamp}"
|
||||
role_key = f"e2e_role_{timestamp}"
|
||||
|
||||
# 1. 通过API创建角色
|
||||
role_response = await authenticated_client.post(
|
||||
"/api/roles",
|
||||
json={
|
||||
"roleName": role_name,
|
||||
"roleKey": role_key,
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
)
|
||||
assert role_response.status_code == 201
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
# 2. 通过前端登录
|
||||
await page.goto("http://localhost:3002/login")
|
||||
await page.fill('input[name="username"]', settings.TEST_USERNAME)
|
||||
await page.fill('input[name="password"]', settings.TEST_PASSWORD)
|
||||
await page.click('button[type="submit"]')
|
||||
await page.wait_for_url("**/")
|
||||
|
||||
# 3. 通过前端创建用户
|
||||
await page.click('text=用户管理')
|
||||
await page.wait_for_url("**/users")
|
||||
|
||||
await page.click('text=创建用户')
|
||||
|
||||
username = f"e2e_user_{timestamp}"
|
||||
await page.fill('input[name="username"]', username)
|
||||
await page.fill('input[name="email"]', f"e2e_{timestamp}@example.com")
|
||||
await page.fill('input[name="password"]', 'Test123!@#')
|
||||
await page.fill('input[name="confirmPassword"]', 'Test123!@#')
|
||||
|
||||
await page.click('button[type="submit"]')
|
||||
await page.wait_for_selector('.success-message', timeout=10000)
|
||||
|
||||
# 4. 通过API获取用户ID并分配角色
|
||||
users_response = await authenticated_client.get("/api/users")
|
||||
users = users_response.json()
|
||||
user = next((u for u in users if u['username'] == username), None)
|
||||
assert user is not None
|
||||
|
||||
await authenticated_client.put(
|
||||
f"/api/users/{user['id']}",
|
||||
json={"roleId": role_id}
|
||||
)
|
||||
|
||||
# 5. 通过API验证角色分配
|
||||
user_response = await authenticated_client.get(f"/api/users/{user['id']}")
|
||||
assert user_response.status_code == 200
|
||||
user_data = user_response.json()
|
||||
assert user_data["roleId"] == role_id
|
||||
|
||||
# 6. 清理测试数据
|
||||
await authenticated_client.delete(f"/api/users/{user['id']}")
|
||||
await authenticated_client.delete(f"/api/roles/{role_id}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_and_navigation_e2e(self, page):
|
||||
"""测试登录和导航 - 前后端联通"""
|
||||
# 1. 访问登录页面
|
||||
await page.goto("http://localhost:3002/login")
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
title = await page.title()
|
||||
assert "登录" in title or "Login" in title.lower()
|
||||
|
||||
# 2. 填写登录表单
|
||||
await page.fill('input[name="username"]', settings.TEST_USERNAME)
|
||||
await page.fill('input[name="password"]', settings.TEST_PASSWORD)
|
||||
|
||||
# 3. 点击登录按钮
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
# 4. 等待跳转到首页
|
||||
await page.wait_for_url("**/", timeout=10000)
|
||||
|
||||
# 5. 验证用户信息显示
|
||||
user_info = await page.query_selector('.user-info')
|
||||
assert user_info is not None, "User info element not found"
|
||||
|
||||
user_text = await user_info.text_content()
|
||||
assert settings.TEST_USERNAME in user_text
|
||||
|
||||
# 6. 测试导航到不同页面
|
||||
await page.click('text=用户管理')
|
||||
await page.wait_for_url("**/users")
|
||||
|
||||
await page.click('text=角色管理')
|
||||
await page.wait_for_url("**/roles")
|
||||
|
||||
await page.click('text=系统配置')
|
||||
await page.wait_for_url("**/config")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_config_e2e(self, page, authenticated_client):
|
||||
"""测试系统配置 - 前后端联通"""
|
||||
# 1. 通过前端登录
|
||||
await page.goto("http://localhost:3002/login")
|
||||
await page.fill('input[name="username"]', settings.TEST_USERNAME)
|
||||
await page.fill('input[name="password"]', settings.TEST_PASSWORD)
|
||||
await page.click('button[type="submit"]')
|
||||
await page.wait_for_url("**/")
|
||||
|
||||
# 2. 通过前端访问系统配置
|
||||
await page.click('text=系统配置')
|
||||
await page.wait_for_url("**/config")
|
||||
|
||||
# 3. 验证配置列表显示
|
||||
table = await page.query_selector('table')
|
||||
assert table is not None, "Config table not found"
|
||||
|
||||
# 4. 通过API获取配置
|
||||
config_response = await authenticated_client.get("/api/config")
|
||||
assert config_response.status_code == 200
|
||||
configs = config_response.json()
|
||||
|
||||
# 5. 验证前后端数据一致
|
||||
page_content = await page.content()
|
||||
for config in configs[:3]:
|
||||
assert config['configKey'] in page_content or config['configName'] in page_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_and_filter_e2e(self, page, authenticated_client):
|
||||
"""测试搜索和过滤 - 前后端联通"""
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
# 1. 通过API创建多个测试用户
|
||||
user_ids = []
|
||||
for i in range(3):
|
||||
username = f"search_{timestamp}_{i}"
|
||||
response = await authenticated_client.post(
|
||||
"/api/users",
|
||||
json={
|
||||
"username": username,
|
||||
"password": "Test123!@#",
|
||||
"email": f"search_{timestamp}_{i}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
user_ids.append(response.json()["id"])
|
||||
|
||||
try:
|
||||
# 2. 通过前端登录
|
||||
await page.goto("http://localhost:3002/login")
|
||||
await page.fill('input[name="username"]', settings.TEST_USERNAME)
|
||||
await page.fill('input[name="password"]', settings.TEST_PASSWORD)
|
||||
await page.click('button[type="submit"]')
|
||||
await page.wait_for_url("**/")
|
||||
|
||||
# 3. 通过前端搜索用户
|
||||
await page.click('text=用户管理')
|
||||
await page.wait_for_url("**/users")
|
||||
|
||||
await page.fill('input[name="keyword"]', f"search_{timestamp}")
|
||||
await page.click('button[type="search"]')
|
||||
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
# 4. 验证搜索结果显示
|
||||
page_content = await page.content()
|
||||
assert f"search_{timestamp}" in page_content
|
||||
|
||||
# 5. 通过API验证搜索结果
|
||||
search_response = await authenticated_client.get(
|
||||
"/api/users/page",
|
||||
params={"keyword": f"search_{timestamp}", "page": 0, "size": 10}
|
||||
)
|
||||
assert search_response.status_code == 200
|
||||
search_data = search_response.json()
|
||||
assert len(search_data["content"]) >= 3
|
||||
|
||||
finally:
|
||||
# 6. 清理测试数据
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/users/{user_id}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,82 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('用户管理 E2E 测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[type="password"]', 'admin123');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForURL('**/dashboard');
|
||||
});
|
||||
|
||||
test('创建用户完整流程', async ({ page }) => {
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users');
|
||||
|
||||
await page.click('text=创建用户');
|
||||
|
||||
const timestamp = Date.now();
|
||||
const username = `testuser_${timestamp}`;
|
||||
|
||||
await page.fill('input[name="username"]', username);
|
||||
await page.fill('input[name="email"]', `test_${timestamp}@example.com`);
|
||||
await page.fill('input[name="phone"]', '13800138000');
|
||||
await page.fill('input[name="password"]', 'Test123!@#');
|
||||
await page.fill('input[name="confirmPassword"]', 'Test123!@#');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
await expect(page.locator('table')).toContainText(username);
|
||||
});
|
||||
|
||||
test('编辑用户流程', async ({ page }) => {
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users');
|
||||
|
||||
await page.click('table tbody tr:first-child .edit-button');
|
||||
|
||||
await page.fill('input[name="email"]', 'updated@example.com');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
await expect(page.locator('table')).toContainText('updated@example.com');
|
||||
});
|
||||
|
||||
test('删除用户流程', async ({ page }) => {
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users');
|
||||
|
||||
const firstRow = page.locator('table tbody tr:first-child');
|
||||
const username = await firstRow.locator('td:first-child').textContent();
|
||||
|
||||
await firstRow.locator('.delete-button').click();
|
||||
|
||||
await page.click('.confirm-dialog .confirm-button');
|
||||
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.locator('table')).not.toContainText(username);
|
||||
});
|
||||
|
||||
test('搜索用户功能', async ({ page }) => {
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users');
|
||||
|
||||
await page.fill('input[name="keyword"]', 'admin');
|
||||
await page.click('button[type="search"]');
|
||||
|
||||
await expect(page.locator('table')).toContainText('admin');
|
||||
});
|
||||
|
||||
test('分页功能', async ({ page }) => {
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users');
|
||||
|
||||
await page.click('.pagination .next-page');
|
||||
|
||||
await expect(page.locator('.pagination .current-page')).toContainText('2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
审计日志测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.audit_api import SysLogAPI
|
||||
|
||||
|
||||
@pytest.mark.audit
|
||||
@pytest.mark.regression
|
||||
class TestLoginLog:
|
||||
"""登录日志测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_login_log(self, authenticated_client):
|
||||
"""测试创建登录日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"username": f"testuser_{timestamp}",
|
||||
"ip": "127.0.0.1",
|
||||
"loginLocation": "本地",
|
||||
"browser": "Chrome",
|
||||
"os": "Mac OS",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
|
||||
response = await api.create_login_log(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["username"] == data["username"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_login_logs(self, authenticated_client):
|
||||
"""测试获取所有登录日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
response = await api.get_login_logs()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_log_by_id(self, authenticated_client):
|
||||
"""测试根据ID获取登录日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"username": f"testuser_{timestamp}",
|
||||
"ip": "127.0.0.1",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
create_response = await api.create_login_log(data)
|
||||
log_id = create_response.json()["id"]
|
||||
|
||||
response = await api.get_login_log_by_id(log_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == log_id
|
||||
|
||||
|
||||
@pytest.mark.audit
|
||||
@pytest.mark.regression
|
||||
class TestExceptionLog:
|
||||
"""异常日志测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_exception_log(self, authenticated_client):
|
||||
"""测试创建异常日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"title": f"测试异常_{timestamp}",
|
||||
"exceptionName": "NullPointerException",
|
||||
"exceptionMsg": "Null pointer at line 100",
|
||||
"methodName": "cn.novalon.manage.sys.service.UserService.getUser",
|
||||
"ip": "127.0.0.1",
|
||||
"exceptionStack": "java.lang.NullPointerException\\n at..."
|
||||
}
|
||||
|
||||
response = await api.create_exception_log(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["title"] == data["title"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_exception_logs(self, authenticated_client):
|
||||
"""测试获取所有异常日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
response = await api.get_exception_logs()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_exception_log_by_id(self, authenticated_client):
|
||||
"""测试根据ID获取异常日志"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"title": f"测试异常_{timestamp}",
|
||||
"exceptionName": "NullPointerException",
|
||||
"exceptionMsg": "Null pointer"
|
||||
}
|
||||
create_response = await api.create_exception_log(data)
|
||||
log_id = create_response.json()["id"]
|
||||
|
||||
response = await api.get_exception_log_by_id(log_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == log_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_logs_by_page_success(self, authenticated_client):
|
||||
"""测试分页获取登录日志成功"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
for i in range(5):
|
||||
timestamp = int(time.time() * 1000) + i
|
||||
data = {
|
||||
"username": f"testuser_{i}",
|
||||
"ip": f"127.0.0.{i}",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
await api.create_login_log(data)
|
||||
|
||||
response = await api.get_login_logs_by_page(page=0, size=10)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "content" in data
|
||||
assert "totalElements" in data
|
||||
assert "totalPages" in data
|
||||
assert "currentPage" in data
|
||||
assert "pageSize" in data
|
||||
assert len(data["content"]) <= 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_logs_by_page_with_sort(self, authenticated_client):
|
||||
"""测试分页获取登录日志并排序成功"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
for i in range(3):
|
||||
timestamp = int(time.time() * 1000) + i
|
||||
data = {
|
||||
"username": f"sortuser_{i}",
|
||||
"ip": "127.0.0.1",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
await api.create_login_log(data)
|
||||
|
||||
response = await api.get_login_logs_by_page(page=0, size=10, sort="username", order="asc")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
usernames = [log["username"] for log in data["content"]]
|
||||
assert usernames == sorted(usernames)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_logs_by_page_with_search(self, authenticated_client):
|
||||
"""测试分页获取登录日志并搜索成功"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
timestamp1 = int(time.time() * 1000)
|
||||
data1 = {
|
||||
"username": "search_test_user",
|
||||
"ip": "127.0.0.1",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
await api.create_login_log(data1)
|
||||
|
||||
timestamp2 = int(time.time() * 1000) + 1
|
||||
data2 = {
|
||||
"username": "other_user",
|
||||
"ip": "127.0.0.2",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
await api.create_login_log(data2)
|
||||
|
||||
response = await api.get_login_logs_by_page(page=0, size=10, keyword="search")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["content"]) >= 1
|
||||
assert all("search" in log["username"] or "search" in log.get("ip", "")
|
||||
for log in data["content"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_log_count_success(self, authenticated_client):
|
||||
"""测试获取登录日志总数成功"""
|
||||
api = SysLogAPI(authenticated_client)
|
||||
|
||||
initial_count_response = await api.get_login_log_count()
|
||||
initial_count = initial_count_response.json()
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"username": f"count_test_user",
|
||||
"ip": "127.0.0.1",
|
||||
"status": "0",
|
||||
"msg": "登录成功"
|
||||
}
|
||||
await api.create_login_log(data)
|
||||
|
||||
final_count_response = await api.get_login_log_count()
|
||||
final_count = final_count_response.json()
|
||||
|
||||
assert final_count == initial_count + 1
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
认证测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from test_utils.api_client.api.auth_api import AuthAPI
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.mark.auth
|
||||
@pytest.mark.smoke
|
||||
class TestAuth:
|
||||
"""认证测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(self, http_client):
|
||||
"""测试成功登录"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.login(settings.TEST_USERNAME, settings.TEST_PASSWORD)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "token" in data
|
||||
assert isinstance(data["token"], str)
|
||||
assert "userId" in data
|
||||
assert "username" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_credentials(self, http_client):
|
||||
"""测试无效凭证登录"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.login("invalid_user", "invalid_password")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_missing_fields(self, http_client):
|
||||
"""测试缺少必填字段"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await http_client.post("/api/auth/login", json={
|
||||
"username": "test"
|
||||
})
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_success(self, http_client):
|
||||
"""测试注册成功"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
response = await http_client.post("/api/auth/register", json={
|
||||
"username": f"testuser_{timestamp}",
|
||||
"password": "password123",
|
||||
"email": f"test_{timestamp}@example.com"
|
||||
})
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["username"] == f"testuser_{timestamp}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_username(self, http_client):
|
||||
"""测试注册重复用户名"""
|
||||
response = await http_client.post("/api/auth/register", json={
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
"email": "admin@example.com"
|
||||
})
|
||||
|
||||
assert response.status_code == 500
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_success(self, http_client):
|
||||
"""测试登出成功"""
|
||||
response = await http_client.post("/api/auth/logout")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
系统配置测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.config_api import SysConfigAPI
|
||||
|
||||
|
||||
@pytest.mark.config
|
||||
@pytest.mark.regression
|
||||
class TestSysConfig:
|
||||
"""系统参数配置测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_config_success(self, authenticated_client):
|
||||
"""测试创建系统配置成功"""
|
||||
api = SysConfigAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"configName": f"测试配置_{timestamp}",
|
||||
"configKey": f"test.config.key.{timestamp}",
|
||||
"configValue": "test_value",
|
||||
"configType": "N"
|
||||
}
|
||||
|
||||
response = await api.create(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["configName"] == data["configName"]
|
||||
assert result["configKey"] == data["configKey"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_configs(self, authenticated_client):
|
||||
"""测试获取所有配置"""
|
||||
api = SysConfigAPI(authenticated_client)
|
||||
|
||||
response = await api.get_all()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_config_by_key(self, authenticated_client):
|
||||
"""测试根据key获取配置"""
|
||||
api = SysConfigAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"configName": f"测试配置_{timestamp}",
|
||||
"configKey": f"test.config.key.{timestamp}",
|
||||
"configValue": "test_value",
|
||||
"configType": "N"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
config_key = data["configKey"]
|
||||
|
||||
response = await api.get_by_key(config_key)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["configKey"] == config_key
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_config(self, authenticated_client):
|
||||
"""测试更新配置"""
|
||||
api = SysConfigAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"configName": f"测试配置_{timestamp}",
|
||||
"configKey": f"test.config.key.{timestamp}",
|
||||
"configValue": "old_value",
|
||||
"configType": "N"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
config_id = create_response.json()["id"]
|
||||
|
||||
update_data = {
|
||||
"configName": f"更新后_{timestamp}",
|
||||
"configKey": f"test.config.key.{timestamp}",
|
||||
"configValue": "new_value",
|
||||
"configType": "N"
|
||||
}
|
||||
response = await api.update(config_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["configValue"] == "new_value"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_config(self, authenticated_client):
|
||||
"""测试删除配置"""
|
||||
api = SysConfigAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"configName": f"测试配置_{timestamp}",
|
||||
"configKey": f"test.config.key.{timestamp}",
|
||||
"configValue": "test_value",
|
||||
"configType": "N"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
config_id = create_response.json()["id"]
|
||||
|
||||
response = await api.delete(config_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
字典管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.dict_api import DictTypeAPI, DictDataAPI
|
||||
|
||||
|
||||
@pytest.mark.dict
|
||||
@pytest.mark.regression
|
||||
class TestDictType:
|
||||
"""字典类型测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dict_type_success(self, authenticated_client):
|
||||
"""测试创建字典类型成功"""
|
||||
api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"dictName": f"测试字典_{timestamp}",
|
||||
"dictType": f"test_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await api.create(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["dictName"] == data["dictName"]
|
||||
assert result["dictType"] == data["dictType"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_dict_types(self, authenticated_client):
|
||||
"""测试获取所有字典类型"""
|
||||
api = DictTypeAPI(authenticated_client)
|
||||
|
||||
response = await api.get_all()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dict_type_by_id(self, authenticated_client):
|
||||
"""测试根据ID获取字典类型"""
|
||||
api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
create_data = {
|
||||
"dictName": f"测试字典_{timestamp}",
|
||||
"dictType": f"test_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(create_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await api.get_by_id(dict_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == dict_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_dict_type(self, authenticated_client):
|
||||
"""测试更新字典类型"""
|
||||
api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
create_data = {
|
||||
"dictName": f"测试字典_{timestamp}",
|
||||
"dictType": f"test_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(create_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
update_data = {
|
||||
"dictName": f"更新后_{timestamp}",
|
||||
"dictType": f"test_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
response = await api.update(dict_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["dictName"] == f"更新后_{timestamp}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dict_type(self, authenticated_client):
|
||||
"""测试删除字典类型"""
|
||||
api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
create_data = {
|
||||
"dictName": f"测试字典_{timestamp}",
|
||||
"dictType": f"test_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(create_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await api.delete(dict_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.dict
|
||||
@pytest.mark.regression
|
||||
class TestDictData:
|
||||
"""字典数据测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dict_data_success(self, authenticated_client):
|
||||
"""测试创建字典数据成功"""
|
||||
dict_type_api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
dict_type_data = {
|
||||
"dictName": f"测试字典类型_{timestamp}",
|
||||
"dictType": f"test_type_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
dict_type_response = await dict_type_api.create(dict_type_data)
|
||||
dict_type_id = dict_type_response.json()["id"]
|
||||
|
||||
dict_data_api = DictDataAPI(authenticated_client)
|
||||
data = {
|
||||
"dictSort": 1,
|
||||
"dictLabel": f"测试标签_{timestamp}",
|
||||
"dictValue": f"test_value_{timestamp}",
|
||||
"dictType": f"test_type_{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await dict_data_api.create(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["dictLabel"] == data["dictLabel"]
|
||||
assert result["dictValue"] == data["dictValue"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dict_data_by_type(self, authenticated_client):
|
||||
"""测试根据类型获取字典数据"""
|
||||
dict_type_api = DictTypeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
dict_type = f"test_type_{timestamp}"
|
||||
dict_type_data = {
|
||||
"dictName": f"测试字典类型_{timestamp}",
|
||||
"dictType": dict_type,
|
||||
"status": "0"
|
||||
}
|
||||
await dict_type_api.create(dict_type_data)
|
||||
|
||||
dict_data_api = DictDataAPI(authenticated_client)
|
||||
data = {
|
||||
"dictSort": 1,
|
||||
"dictLabel": f"测试标签_{timestamp}",
|
||||
"dictValue": f"test_value_{timestamp}",
|
||||
"dictType": dict_type,
|
||||
"status": "0"
|
||||
}
|
||||
await dict_data_api.create(data)
|
||||
|
||||
response = await dict_data_api.get_by_type(dict_type)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert len(result) > 0
|
||||
assert result[0]["dictType"] == dict_type
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
字典管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from test_utils.api_client.api.dictionary_api import DictionaryAPI
|
||||
|
||||
|
||||
@pytest.mark.dictionary
|
||||
@pytest.mark.regression
|
||||
class TestDictionary:
|
||||
"""字典管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试创建字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["type"] == test_dictionary_data["type"]
|
||||
assert data["code"] == test_dictionary_data["code"]
|
||||
assert data["name"] == test_dictionary_data["name"]
|
||||
|
||||
cleanup_dictionary.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dictionary_duplicate_type_code(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试创建重复类型和编码"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionary_by_id_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试根据ID获取字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.get_dictionary_by_id(dict_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == dict_id
|
||||
assert data["type"] == test_dictionary_data["type"]
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionary_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的字典"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.get_dictionary_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionaries_by_type_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试根据类型获取字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.get_dictionaries_by_type(test_dictionary_data["type"])
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert any(d["id"] == dict_id for d in data)
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_dictionaries_success(self, authenticated_client):
|
||||
"""测试获取所有字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.get_all_dictionaries()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试更新字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"name": "Updated name"}
|
||||
response = await dict_api.update_dictionary(dict_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Updated name"
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试删除字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.delete_dictionary(dict_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_type_and_code_exists_true(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试检查类型和编码存在-返回true"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.check_type_and_code_exists(
|
||||
test_dictionary_data["type"],
|
||||
test_dictionary_data["code"]
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_type_and_code_exists_false(self, authenticated_client):
|
||||
"""测试检查类型和编码存在-返回false"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.check_type_and_code_exists("NONEXISTENT_TYPE", "NONEXISTENT_CODE")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
异常场景测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.user_api import UserAPI
|
||||
from test_utils.api_client.api.role_api import RoleAPI
|
||||
from test_utils.api_client.api.notice_api import SysNoticeAPI
|
||||
|
||||
|
||||
@pytest.mark.exception
|
||||
@pytest.mark.regression
|
||||
class TestExceptionScenarios:
|
||||
"""异常场景测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_with_duplicate_username(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试创建重复用户名的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
assert create_response.status_code == 201
|
||||
user_id = create_response.json()["id"]
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
duplicate_response = await user_api.create_user(test_user_data)
|
||||
assert duplicate_response.status_code in [400, 409]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_with_invalid_email(self, authenticated_client):
|
||||
"""测试创建邮箱格式无效的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
invalid_emails = [
|
||||
"invalid-email",
|
||||
"@example.com",
|
||||
"user@",
|
||||
"user@domain",
|
||||
"user name@example.com"
|
||||
]
|
||||
|
||||
for invalid_email in invalid_emails:
|
||||
timestamp = int(time.time() * 1000)
|
||||
user_data = {
|
||||
"username": f"test_{timestamp}",
|
||||
"password": "Test123!@#",
|
||||
"email": invalid_email,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
response = await user_api.create_user(user_data)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_with_weak_password(self, authenticated_client):
|
||||
"""测试创建弱密码用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
weak_passwords = [
|
||||
"123456",
|
||||
"password",
|
||||
"qwerty",
|
||||
"111111",
|
||||
"abc123"
|
||||
]
|
||||
|
||||
for weak_password in weak_passwords:
|
||||
timestamp = int(time.time() * 1000)
|
||||
user_data = {
|
||||
"username": f"test_{timestamp}",
|
||||
"password": weak_password,
|
||||
"email": f"test_{timestamp}@example.com",
|
||||
"status": 1
|
||||
}
|
||||
|
||||
response = await user_api.create_user(user_data)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_with_missing_fields(self, authenticated_client):
|
||||
"""测试创建缺少必填字段的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
missing_field_scenarios = [
|
||||
{"password": "Test123!@#", "email": "test@example.com"},
|
||||
{"username": "testuser", "email": "test@example.com"},
|
||||
{"username": "testuser", "password": "Test123!@#"}
|
||||
]
|
||||
|
||||
for scenario in missing_field_scenarios:
|
||||
response = await user_api.create_user(scenario)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nonexistent_user(self, authenticated_client):
|
||||
"""测试更新不存在的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
update_data = {"email": "updated@example.com"}
|
||||
response = await user_api.update_user(999999, update_data)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_user(self, authenticated_client):
|
||||
"""测试删除不存在的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
response = await user_api.delete_user(999999)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_with_duplicate_key(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试创建重复角色键的角色"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
assert create_response.status_code == 201
|
||||
role_id = create_response.json()["id"]
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
duplicate_response = await role_api.create_role(test_role_data)
|
||||
assert duplicate_response.status_code in [400, 409]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_with_invalid_status(self, authenticated_client):
|
||||
"""测试创建状态无效的角色"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
invalid_statuses = ["2", "3", "invalid", "true", "false"]
|
||||
|
||||
for invalid_status in invalid_statuses:
|
||||
timestamp = int(time.time() * 1000)
|
||||
role_data = {
|
||||
"roleName": f"TestRole_{timestamp}",
|
||||
"roleKey": f"test_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": invalid_status
|
||||
}
|
||||
|
||||
response = await role_api.create_role(role_data)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nonexistent_role(self, authenticated_client):
|
||||
"""测试更新不存在的角色"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
update_data = {"roleName": "Updated Role"}
|
||||
response = await role_api.update_role(999999, update_data)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_notice_with_invalid_type(self, authenticated_client):
|
||||
"""测试创建类型无效的公告"""
|
||||
notice_api = SysNoticeAPI(authenticated_client)
|
||||
|
||||
invalid_types = ["3", "4", "invalid", "true", "false"]
|
||||
|
||||
for invalid_type in invalid_types:
|
||||
timestamp = int(time.time() * 1000)
|
||||
notice_data = {
|
||||
"noticeTitle": f"TestNotice_{timestamp}",
|
||||
"noticeType": invalid_type,
|
||||
"noticeContent": "Test content",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await notice_api.create(notice_data)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_notice_with_empty_content(self, authenticated_client):
|
||||
"""测试创建内容为空的公告"""
|
||||
notice_api = SysNoticeAPI(authenticated_client)
|
||||
|
||||
empty_content_scenarios = [
|
||||
{"noticeTitle": "Test", "noticeType": "1", "noticeContent": "", "status": "0"},
|
||||
{"noticeTitle": "", "noticeType": "1", "noticeContent": "Test", "status": "0"},
|
||||
{"noticeTitle": "Test", "noticeType": "1", "noticeContent": " ", "status": "0"}
|
||||
]
|
||||
|
||||
for scenario in empty_content_scenarios:
|
||||
response = await notice_api.create(scenario)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nonexistent_notice(self, authenticated_client):
|
||||
"""测试更新不存在的公告"""
|
||||
notice_api = SysNoticeAPI(authenticated_client)
|
||||
|
||||
update_data = {"noticeTitle": "Updated Notice"}
|
||||
response = await notice_api.update(999999, update_data)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_notice(self, authenticated_client):
|
||||
"""测试删除不存在的公告"""
|
||||
notice_api = SysNoticeAPI(authenticated_client)
|
||||
|
||||
response = await notice_api.delete(999999)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_with_invalid_id(self, authenticated_client):
|
||||
"""测试获取ID无效的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
invalid_ids = [-1, 0, "abc", "1.5", "999999999999"]
|
||||
|
||||
for invalid_id in invalid_ids:
|
||||
try:
|
||||
response = await user_api.get_user_by_id(int(invalid_id) if isinstance(invalid_id, (int, str)) else invalid_id)
|
||||
assert response.status_code in [400, 404]
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pagination_with_invalid_params(self, authenticated_client):
|
||||
"""测试分页参数无效的查询"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
invalid_params = [
|
||||
{"page": -1, "size": 10},
|
||||
{"page": 0, "size": -1},
|
||||
{"page": 0, "size": 0},
|
||||
{"page": 0, "size": 10000},
|
||||
{"page": "abc", "size": 10},
|
||||
{"page": 0, "size": "abc"}
|
||||
]
|
||||
|
||||
for params in invalid_params:
|
||||
try:
|
||||
response = await user_api.get_users_by_page(**params)
|
||||
assert response.status_code in [400, 422]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_special_characters(self, authenticated_client):
|
||||
"""测试搜索特殊字符"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
special_chars = [
|
||||
"<script>alert('xss')</script>",
|
||||
"'; DROP TABLE users; --",
|
||||
"../../../etc/passwd",
|
||||
"{{7*7}}",
|
||||
"%00%00%00%00"
|
||||
]
|
||||
|
||||
for search_term in special_chars:
|
||||
response = await user_api.get_users_by_page(keyword=search_term)
|
||||
assert response.status_code in [200, 400]
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "content" in data
|
||||
for user in data["content"]:
|
||||
assert search_term.lower() not in str(user).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_same_resource_update(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试并发更新同一资源"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
assert create_response.status_code == 201
|
||||
user_id = create_response.json()["id"]
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
import asyncio
|
||||
update_tasks = [
|
||||
user_api.update_user(user_id, {"email": f"concurrent1_{time.time()}@example.com"}),
|
||||
user_api.update_user(user_id, {"email": f"concurrent2_{time.time()}@example.com"}),
|
||||
user_api.update_user(user_id, {"email": f"concurrent3_{time.time()}@example.com"})
|
||||
]
|
||||
|
||||
results = await asyncio.gather(*update_tasks, return_exceptions=True)
|
||||
|
||||
successful_updates = sum(1 for r in results if r.status_code == 200)
|
||||
assert successful_updates >= 1, "至少应该有一个更新成功"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_large_payload_handling(self, authenticated_client):
|
||||
"""测试大数据负载处理"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
large_content = "x" * 10000
|
||||
user_data = {
|
||||
"username": f"large_payload_{int(time.time() * 1000)}",
|
||||
"password": "Test123!@#",
|
||||
"email": f"large_{int(time.time() * 1000)}@example.com",
|
||||
"phone": large_content
|
||||
}
|
||||
|
||||
response = await user_api.create_user(user_data)
|
||||
assert response.status_code in [201, 400, 413]
|
||||
|
||||
if response.status_code in [400, 413]:
|
||||
logger.info("系统正确拒绝了过大的负载")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unauthorized_access(self, http_client):
|
||||
"""测试未授权访问"""
|
||||
user_api = UserAPI(http_client)
|
||||
|
||||
response = await user_api.get_all_users()
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limiting(self, authenticated_client):
|
||||
"""测试速率限制"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
requests_made = 0
|
||||
rate_limit_hit = False
|
||||
|
||||
for i in range(100):
|
||||
response = await user_api.get_all_users()
|
||||
requests_made += 1
|
||||
|
||||
if response.status_code == 429:
|
||||
rate_limit_hit = True
|
||||
logger.info(f"速率限制在第 {requests_made} 个请求时触发")
|
||||
break
|
||||
|
||||
if rate_limit_hit:
|
||||
logger.info("系统正确实施了速率限制")
|
||||
else:
|
||||
logger.info("未触发速率限制(可能未配置或阈值较高)")
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
文件管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
from test_utils.api_client.api.file_api import SysFileAPI
|
||||
|
||||
|
||||
@pytest.mark.file
|
||||
@pytest.mark.regression
|
||||
class TestSysFile:
|
||||
"""文件管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_file(self, authenticated_client):
|
||||
"""测试文件上传"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
test_file_path = "/tmp/test_file.txt"
|
||||
|
||||
with open(test_file_path, "w") as f:
|
||||
f.write("This is a test file content")
|
||||
|
||||
response = await api.upload(test_file_path, "test_user")
|
||||
|
||||
os.remove(test_file_path)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert "id" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_files(self, authenticated_client):
|
||||
"""测试获取所有文件"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
|
||||
response = await api.get_all()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_file_by_id(self, authenticated_client):
|
||||
"""测试根据ID获取文件"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
test_file_path = "/tmp/test_file.txt"
|
||||
|
||||
with open(test_file_path, "w") as f:
|
||||
f.write("Test content")
|
||||
|
||||
upload_response = await api.upload(test_file_path, "test_user")
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
os.remove(test_file_path)
|
||||
|
||||
response = await api.get_by_id(file_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == file_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_file(self, authenticated_client):
|
||||
"""测试文件下载"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
test_file_path = "/tmp/test_file.txt"
|
||||
|
||||
with open(test_file_path, "w") as f:
|
||||
f.write("Download test content")
|
||||
|
||||
upload_response = await api.upload(test_file_path, "test_user")
|
||||
file_name = upload_response.json()["filePath"].split("/")[-1]
|
||||
|
||||
os.remove(test_file_path)
|
||||
|
||||
response = await api.download(file_name)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_file(self, authenticated_client):
|
||||
"""测试文件预览"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
test_file_path = "/tmp/test_file.txt"
|
||||
|
||||
with open(test_file_path, "w") as f:
|
||||
f.write("Preview test content")
|
||||
|
||||
upload_response = await api.upload(test_file_path, "test_user")
|
||||
file_name = upload_response.json()["filePath"].split("/")[-1]
|
||||
|
||||
os.remove(test_file_path)
|
||||
|
||||
response = await api.preview(file_name)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_file(self, authenticated_client):
|
||||
"""测试删除文件"""
|
||||
api = SysFileAPI(authenticated_client)
|
||||
test_file_path = "/tmp/test_file.txt"
|
||||
|
||||
with open(test_file_path, "w") as f:
|
||||
f.write("Delete test content")
|
||||
|
||||
upload_response = await api.upload(test_file_path, "test_user")
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
os.remove(test_file_path)
|
||||
|
||||
response = await api.delete(file_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
菜单管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.menu_api import MenuAPI
|
||||
|
||||
|
||||
@pytest.mark.menu
|
||||
@pytest.mark.regression
|
||||
class TestMenu:
|
||||
"""菜单管理测试类"""
|
||||
|
||||
@pytest.fixture
|
||||
def test_menu_data(self):
|
||||
"""测试菜单数据"""
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"menuName": f"测试菜单_{timestamp}",
|
||||
"parentId": 0,
|
||||
"orderNum": 1,
|
||||
"menuType": "C",
|
||||
"perms": f"system:menu:{timestamp}",
|
||||
"component": f"menu/component/{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_menu(self, authenticated_client):
|
||||
"""清理测试菜单"""
|
||||
menu_ids = []
|
||||
|
||||
yield menu_ids
|
||||
|
||||
for menu_id in menu_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/menus/{menu_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_menu_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试创建菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
response = await menu_api.create_menu(test_menu_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["menuName"] == test_menu_data["menuName"]
|
||||
assert data["parentId"] == test_menu_data["parentId"]
|
||||
assert data["menuType"] == test_menu_data["menuType"]
|
||||
|
||||
cleanup_menu.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_menu_by_id_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试根据ID获取菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
create_response = await menu_api.create_menu(test_menu_data)
|
||||
menu_id = create_response.json()["id"]
|
||||
|
||||
response = await menu_api.get_menu_by_id(menu_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == menu_id
|
||||
assert data["menuName"] == test_menu_data["menuName"]
|
||||
|
||||
cleanup_menu.append(menu_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_menu_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的菜单"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
response = await menu_api.get_menu_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_menus_success(self, authenticated_client):
|
||||
"""测试获取所有菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
response = await menu_api.get_all_menus()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_menu_tree_success(self, authenticated_client):
|
||||
"""测试获取菜单树成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
response = await menu_api.get_menu_tree()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_menu_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试更新菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
create_response = await menu_api.create_menu(test_menu_data)
|
||||
menu_id = create_response.json()["id"]
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
update_data = {
|
||||
"menuName": f"更新后菜单_{timestamp}",
|
||||
"orderNum": 2
|
||||
}
|
||||
response = await menu_api.update_menu(menu_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["menuName"] == f"更新后菜单_{timestamp}"
|
||||
assert data["orderNum"] == 2
|
||||
|
||||
cleanup_menu.append(menu_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_menu_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试删除菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
create_response = await menu_api.create_menu(test_menu_data)
|
||||
menu_id = create_response.json()["id"]
|
||||
|
||||
response = await menu_api.delete_menu(menu_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_menus_by_parent_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试根据父菜单ID获取子菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
parent_response = await menu_api.create_menu(test_menu_data)
|
||||
parent_id = parent_response.json()["id"]
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
child_menu_data = test_menu_data.copy()
|
||||
child_menu_data["menuName"] = f"子菜单_{timestamp}"
|
||||
child_menu_data["parentId"] = parent_id
|
||||
|
||||
child_response = await menu_api.create_menu(child_menu_data)
|
||||
child_id = child_response.json()["id"]
|
||||
|
||||
response = await menu_api.get_menus_by_parent(parent_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert any(menu["id"] == child_id for menu in data)
|
||||
|
||||
cleanup_menu.extend([parent_id, child_id])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_menus_by_type_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试根据菜单类型获取菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
create_response = await menu_api.create_menu(test_menu_data)
|
||||
menu_id = create_response.json()["id"]
|
||||
|
||||
response = await menu_api.get_menus_by_type("C")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert any(menu["id"] == menu_id for menu in data)
|
||||
|
||||
cleanup_menu.append(menu_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_menu_with_parent_success(self, authenticated_client, test_menu_data, cleanup_menu):
|
||||
"""测试创建带父菜单的子菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
|
||||
parent_response = await menu_api.create_menu(test_menu_data)
|
||||
parent_id = parent_response.json()["id"]
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
child_menu_data = test_menu_data.copy()
|
||||
child_menu_data["menuName"] = f"子菜单_{timestamp}"
|
||||
child_menu_data["parentId"] = parent_id
|
||||
|
||||
response = await menu_api.create_menu(child_menu_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["parentId"] == parent_id
|
||||
|
||||
cleanup_menu.extend([parent_id, data["id"]])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_menu_directory_type_success(self, authenticated_client, cleanup_menu):
|
||||
"""测试创建目录类型菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
menu_data = {
|
||||
"menuName": f"目录_{timestamp}",
|
||||
"parentId": 0,
|
||||
"orderNum": 1,
|
||||
"menuType": "M",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await menu_api.create_menu(menu_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["menuType"] == "M"
|
||||
|
||||
cleanup_menu.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_menu_button_type_success(self, authenticated_client, cleanup_menu):
|
||||
"""测试创建按钮类型菜单成功"""
|
||||
menu_api = MenuAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
menu_data = {
|
||||
"menuName": f"按钮_{timestamp}",
|
||||
"parentId": 1,
|
||||
"orderNum": 1,
|
||||
"menuType": "F",
|
||||
"perms": f"system:button:{timestamp}",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await menu_api.create_menu(menu_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["menuType"] == "F"
|
||||
|
||||
cleanup_menu.append(data["id"])
|
||||
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
通知公告测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from test_utils.api_client.api.notice_api import SysNoticeAPI, SysMessageAPI
|
||||
|
||||
|
||||
@pytest.mark.notice
|
||||
@pytest.mark.regression
|
||||
class TestSysNotice:
|
||||
"""系统公告测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_notice_success(self, authenticated_client):
|
||||
"""测试创建公告成功"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"noticeTitle": f"测试公告_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "这是测试公告内容",
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
response = await api.create(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["noticeTitle"] == data["noticeTitle"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_notices(self, authenticated_client):
|
||||
"""测试获取所有公告"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
|
||||
response = await api.get_all()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_notice_by_id(self, authenticated_client):
|
||||
"""测试根据ID获取公告"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"noticeTitle": f"测试公告_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "测试内容",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
notice_id = create_response.json()["id"]
|
||||
|
||||
response = await api.get_by_id(notice_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == notice_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_notice_by_status(self, authenticated_client):
|
||||
"""测试根据状态获取公告"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"noticeTitle": f"测试公告_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "测试内容",
|
||||
"status": "0"
|
||||
}
|
||||
await api.create(data)
|
||||
|
||||
response = await api.get_by_status("0")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_notice(self, authenticated_client):
|
||||
"""测试更新公告"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"noticeTitle": f"测试公告_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "原始内容",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
notice_id = create_response.json()["id"]
|
||||
|
||||
update_data = {
|
||||
"noticeTitle": f"更新后_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "更新后内容",
|
||||
"status": "0"
|
||||
}
|
||||
response = await api.update(notice_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["noticeTitle"] == f"更新后_{timestamp}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_notice(self, authenticated_client):
|
||||
"""测试删除公告"""
|
||||
api = SysNoticeAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"noticeTitle": f"测试公告_{timestamp}",
|
||||
"noticeType": "1",
|
||||
"noticeContent": "测试内容",
|
||||
"status": "0"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
notice_id = create_response.json()["id"]
|
||||
|
||||
response = await api.delete(notice_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.notice
|
||||
@pytest.mark.regression
|
||||
class TestSysMessage:
|
||||
"""用户消息测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_message(self, authenticated_client):
|
||||
"""测试创建消息"""
|
||||
api = SysMessageAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"userId": 1,
|
||||
"title": f"测试消息_{timestamp}",
|
||||
"content": "这是测试消息内容",
|
||||
"type": "1"
|
||||
}
|
||||
|
||||
response = await api.create(data)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["title"] == data["title"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_by_user(self, authenticated_client):
|
||||
"""测试获取用户消息"""
|
||||
api = SysMessageAPI(authenticated_client)
|
||||
user_id = 1
|
||||
|
||||
response = await api.get_by_user(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_unread_count(self, authenticated_client):
|
||||
"""测试获取未读消息数量"""
|
||||
api = SysMessageAPI(authenticated_client)
|
||||
user_id = 1
|
||||
|
||||
response = await api.get_unread_count(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_message_as_read(self, authenticated_client):
|
||||
"""测试标记消息为已读"""
|
||||
api = SysMessageAPI(authenticated_client)
|
||||
timestamp = int(time.time() * 1000)
|
||||
data = {
|
||||
"userId": 1,
|
||||
"title": f"测试消息_{timestamp}",
|
||||
"content": "测试内容",
|
||||
"type": "1"
|
||||
}
|
||||
create_response = await api.create(data)
|
||||
message_id = create_response.json()["id"]
|
||||
|
||||
response = await api.mark_as_read(message_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
权限管理增强测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from test_utils.api_client.api.role_api import RoleAPI
|
||||
from test_utils.api_client.api.user_api import UserAPI
|
||||
|
||||
|
||||
@pytest.mark.permission
|
||||
@pytest.mark.regression
|
||||
class TestPermission:
|
||||
"""权限管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_role_assignment(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试用户角色分配"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
user_response = await user_api.create_user(test_user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
update_data = {"roleId": role_id}
|
||||
response = await user_api.update_user(user_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["roleId"] == role_id
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_role_removal(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试用户角色移除"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
user_response = await user_api.create_user(test_user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
response = await user_api.update_user(user_id, {"roleId": None})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["roleId"] is None
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_status_permission(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试角色状态权限控制"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.update_role(role_id, {"status": 0})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == 0
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_users_same_role(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试多个用户分配相同角色"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_ids = []
|
||||
for i in range(3):
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
user_data = test_user_data.copy()
|
||||
user_data["username"] = f"testuser_{timestamp}_{i}"
|
||||
user_data["email"] = f"test_{timestamp}_{i}@example.com"
|
||||
|
||||
user_response = await user_api.create_user(user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
user_ids.append(user_id)
|
||||
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
for user_id in user_ids:
|
||||
user_response = await user_api.get_user_by_id(user_id)
|
||||
assert user_response.json()["roleId"] == role_id
|
||||
|
||||
cleanup_user.extend(user_ids)
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_hierarchy(self, authenticated_client, cleanup_role):
|
||||
"""测试角色层级"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
admin_role_data = {
|
||||
"roleName": f"Admin_{timestamp}",
|
||||
"roleKey": f"admin_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
admin_response = await role_api.create_role(admin_role_data)
|
||||
admin_id = admin_response.json()["id"]
|
||||
|
||||
user_role_data = {
|
||||
"roleName": f"User_{timestamp}",
|
||||
"roleKey": f"user_{timestamp}",
|
||||
"roleSort": 2,
|
||||
"status": 1
|
||||
}
|
||||
user_response = await role_api.create_role(user_role_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
all_roles = await role_api.get_all_roles()
|
||||
roles_data = all_roles.json()
|
||||
role_sorts = [role["roleSort"] for role in roles_data]
|
||||
|
||||
assert 1 in role_sorts
|
||||
assert 2 in role_sorts
|
||||
|
||||
cleanup_role.extend([admin_id, user_id])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_inheritance(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试权限继承"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_response = await user_api.create_user(test_user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
user_data = await user_api.get_user_by_id(user_id)
|
||||
assert user_data.json()["roleId"] == role_id
|
||||
|
||||
role_data = await role_api.get_role_by_id(role_id)
|
||||
assert role_data.json()["id"] == role_id
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_sort_order(self, authenticated_client, cleanup_role):
|
||||
"""测试角色排序"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
role1_data = {
|
||||
"roleName": f"Role1_{timestamp}",
|
||||
"roleKey": f"role1_{timestamp}",
|
||||
"roleSort": 3,
|
||||
"status": 1
|
||||
}
|
||||
role1_response = await role_api.create_role(role1_data)
|
||||
role1_id = role1_response.json()["id"]
|
||||
|
||||
role2_data = {
|
||||
"roleName": f"Role2_{timestamp}",
|
||||
"roleKey": f"role2_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
role2_response = await role_api.create_role(role2_data)
|
||||
role2_id = role2_response.json()["id"]
|
||||
|
||||
role3_data = {
|
||||
"roleName": f"Role3_{timestamp}",
|
||||
"roleKey": f"role3_{timestamp}",
|
||||
"roleSort": 2,
|
||||
"status": 1
|
||||
}
|
||||
role3_response = await role_api.create_role(role3_data)
|
||||
role3_id = role3_response.json()["id"]
|
||||
|
||||
response = await role_api.get_roles_by_page(page=0, size=10, sort="roleSort", order="asc")
|
||||
roles = response.json()["content"]
|
||||
|
||||
role_sorts = [role["roleSort"] for role in roles]
|
||||
assert role_sorts == sorted(role_sorts)
|
||||
|
||||
cleanup_role.extend([role1_id, role2_id, role3_id])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_role_access(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试禁用角色的访问控制"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_response = await user_api.create_user(test_user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
await role_api.update_role(role_id, {"status": 0})
|
||||
|
||||
role_data = await role_api.get_role_by_id(role_id)
|
||||
assert role_data.json()["status"] == 0
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_uniqueness(self, authenticated_client, cleanup_role):
|
||||
"""测试角色唯一性约束"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
role_data = {
|
||||
"roleName": f"UniqueRole_{timestamp}",
|
||||
"roleKey": f"unique_role_{timestamp}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
response1 = await role_api.create_role(role_data)
|
||||
assert response1.status_code == 201
|
||||
role_id = response1.json()["id"]
|
||||
|
||||
response2 = await role_api.create_role(role_data)
|
||||
assert response2.status_code in [400, 409]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_deletion_with_users(self, authenticated_client, test_user_data, test_role_data, cleanup_user, cleanup_role):
|
||||
"""测试删除有用户的角色"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
role_response = await role_api.create_role(test_role_data)
|
||||
role_id = role_response.json()["id"]
|
||||
|
||||
user_response = await user_api.create_user(test_user_data)
|
||||
user_id = user_response.json()["id"]
|
||||
|
||||
await user_api.update_user(user_id, {"roleId": role_id})
|
||||
|
||||
delete_response = await role_api.delete_role(role_id)
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
user_data = await user_api.get_user_by_id(user_id)
|
||||
assert user_data.json()["roleId"] is None
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
cleanup_role.append(role_id)
|
||||
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
角色管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from test_utils.api_client.api.role_api import RoleAPI
|
||||
|
||||
|
||||
@pytest.mark.role
|
||||
@pytest.mark.regression
|
||||
class TestRole:
|
||||
"""角色管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试创建角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.create_role(test_role_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["roleName"] == test_role_data["roleName"]
|
||||
assert data["roleKey"] == test_role_data["roleKey"]
|
||||
assert data["roleSort"] == test_role_data["roleSort"]
|
||||
assert data["status"] == test_role_data["status"]
|
||||
|
||||
cleanup_role.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_duplicate_name(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试创建重复角色名"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.create_role(test_role_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_id_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试根据ID获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.get_role_by_id(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == role_id
|
||||
assert data["roleName"] == test_role_data["roleName"]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的角色"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.get_role_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_name_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试根据名称获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.get_role_by_name(test_role_data["roleName"])
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["roleName"] == test_role_data["roleName"]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_roles_success(self, authenticated_client):
|
||||
"""测试获取所有角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.get_all_roles()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试更新角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
update_data = {"roleName": f"UPDATED_ROLE_{timestamp}"}
|
||||
response = await role_api.update_role(role_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["roleName"] == f"UPDATED_ROLE_{timestamp}"
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试删除角色成功(逻辑删除)"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.delete_role(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["deletedAt"] is not None
|
||||
|
||||
get_response = await role_api.get_role_by_id(role_id)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试恢复角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
await role_api.delete_role(role_id)
|
||||
|
||||
response = await role_api.restore_role(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
get_response = await role_api.get_role_by_id(role_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_name_exists_true(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试检查角色名存在-返回true"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.check_name_exists(test_role_data["roleName"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_name_exists_false(self, authenticated_client):
|
||||
"""测试检查角色名存在-返回false"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.check_name_exists("NONEXISTENT_ROLE")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_roles_by_page_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试分页获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
for i in range(5):
|
||||
timestamp = int(time.time() * 1000)
|
||||
role_data = {
|
||||
"roleName": f"testrole_{timestamp}_{i}",
|
||||
"roleKey": f"testrole_{timestamp}_{i}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response = await role_api.create_role(role_data)
|
||||
cleanup_role.append(create_response.json()["id"])
|
||||
|
||||
response = await role_api.get_roles_by_page(page=0, size=10)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "content" in data
|
||||
assert "totalElements" in data
|
||||
assert "totalPages" in data
|
||||
assert "currentPage" in data
|
||||
assert "pageSize" in data
|
||||
assert len(data["content"]) <= 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_roles_by_page_with_sort(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试分页获取角色并排序成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
timestamp1 = int(time.time() * 1000)
|
||||
role1_data = {
|
||||
"roleName": f"role_a_{timestamp1}",
|
||||
"roleKey": f"role_a_{timestamp1}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response1 = await role_api.create_role(role1_data)
|
||||
cleanup_role.append(create_response1.json()["id"])
|
||||
|
||||
timestamp2 = int(time.time() * 1000)
|
||||
role2_data = {
|
||||
"roleName": f"role_b_{timestamp2}",
|
||||
"roleKey": f"role_b_{timestamp2}",
|
||||
"roleSort": 2,
|
||||
"status": 1
|
||||
}
|
||||
create_response2 = await role_api.create_role(role2_data)
|
||||
cleanup_role.append(create_response2.json()["id"])
|
||||
|
||||
response = await role_api.get_roles_by_page(page=0, size=10, sort="roleName", order="asc")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
role_names = [role["roleName"] for role in data["content"]]
|
||||
assert role_names == sorted(role_names)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_roles_by_page_with_search(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试分页获取角色并搜索成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
timestamp1 = int(time.time() * 1000)
|
||||
role1_data = {
|
||||
"roleName": f"search_test_role_{timestamp1}",
|
||||
"roleKey": f"search_test_role_{timestamp1}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response1 = await role_api.create_role(role1_data)
|
||||
cleanup_role.append(create_response1.json()["id"])
|
||||
|
||||
timestamp2 = int(time.time() * 1000)
|
||||
role2_data = {
|
||||
"roleName": f"other_role_{timestamp2}",
|
||||
"roleKey": f"other_role_{timestamp2}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response2 = await role_api.create_role(role2_data)
|
||||
cleanup_role.append(create_response2.json()["id"])
|
||||
|
||||
response = await role_api.get_roles_by_page(page=0, size=10, keyword="search")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["content"]) >= 1
|
||||
assert all("search" in role["roleName"] or "search" in role["roleKey"]
|
||||
for role in data["content"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_count_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试获取角色总数成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
initial_count_response = await role_api.get_role_count()
|
||||
initial_count = initial_count_response.json()
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
cleanup_role.append(create_response.json()["id"])
|
||||
|
||||
final_count_response = await role_api.get_role_count()
|
||||
final_count = final_count_response.json()
|
||||
|
||||
assert final_count == initial_count + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_roles_by_page_with_different_page_sizes(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试不同页面大小的分页获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
for i in range(15):
|
||||
timestamp = int(time.time() * 1000)
|
||||
role_data = {
|
||||
"roleName": f"pagesize_test_{timestamp}_{i}",
|
||||
"roleKey": f"pagesize_test_{timestamp}_{i}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response = await role_api.create_role(role_data)
|
||||
cleanup_role.append(create_response.json()["id"])
|
||||
|
||||
response_size_10 = await role_api.get_roles_by_page(page=0, size=10)
|
||||
assert response_size_10.status_code == 200
|
||||
data_size_10 = response_size_10.json()
|
||||
assert len(data_size_10["content"]) == 10
|
||||
|
||||
response_size_5 = await role_api.get_roles_by_page(page=0, size=5)
|
||||
assert response_size_5.status_code == 200
|
||||
data_size_5 = response_size_5.json()
|
||||
assert len(data_size_5["content"]) == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_roles_by_page_with_page_navigation(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试分页导航成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
import time
|
||||
for i in range(25):
|
||||
timestamp = int(time.time() * 1000)
|
||||
role_data = {
|
||||
"roleName": f"pagination_test_{timestamp}_{i}",
|
||||
"roleKey": f"pagination_test_{timestamp}_{i}",
|
||||
"roleSort": 1,
|
||||
"status": 1
|
||||
}
|
||||
create_response = await role_api.create_role(role_data)
|
||||
cleanup_role.append(create_response.json()["id"])
|
||||
|
||||
page1_response = await role_api.get_roles_by_page(page=0, size=10)
|
||||
page1_data = page1_response.json()
|
||||
assert page1_data["currentPage"] == 0
|
||||
assert len(page1_data["content"]) == 10
|
||||
|
||||
page2_response = await role_api.get_roles_by_page(page=1, size=10)
|
||||
page2_data = page2_response.json()
|
||||
assert page2_data["currentPage"] == 1
|
||||
assert len(page2_data["content"]) == 10
|
||||
|
||||
page3_response = await role_api.get_roles_by_page(page=2, size=10)
|
||||
page3_data = page3_response.json()
|
||||
assert page3_data["currentPage"] == 2
|
||||
assert len(page3_data["content"]) >= 5
|
||||
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
用户管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from test_utils.api_client.api.user_api import UserAPI
|
||||
from config import settings
|
||||
|
||||
|
||||
@pytest.mark.user
|
||||
@pytest.mark.regression
|
||||
class TestUser:
|
||||
"""用户管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试创建用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.create_user(test_user_data)
|
||||
|
||||
print(f"Response status: {response.status_code}")
|
||||
print(f"Response text: {response.text}")
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["username"] == test_user_data["username"]
|
||||
assert data["email"] == test_user_data["email"]
|
||||
assert "password" not in data or data["password"] != test_user_data["password"]
|
||||
|
||||
cleanup_user.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_duplicate_username(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试创建重复用户名"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
await user_api.create_user(test_user_data)
|
||||
response = await user_api.create_user(test_user_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试根据ID获取用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.get_user_by_id(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == user_id
|
||||
assert data["username"] == test_user_data["username"]
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.get_user_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_users_success(self, authenticated_client):
|
||||
"""测试获取所有用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.get_all_users()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试更新用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"email": "updated@example.com"}
|
||||
response = await user_api.update_user(user_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["email"] == "updated@example.com"
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试删除用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.delete_user(user_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logical_delete_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试逻辑删除用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.logical_delete_user(user_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
get_deleted_response = await user_api.get_all_users(include_deleted=True)
|
||||
deleted_users = get_deleted_response.json()
|
||||
assert any(u["id"] == user_id for u in deleted_users)
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试恢复用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
await user_api.logical_delete_user(user_id)
|
||||
|
||||
response = await user_api.restore_user(user_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_username_exists_true(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试检查用户名存在-返回true"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.check_username_exists(test_user_data["username"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_username_exists_false(self, authenticated_client):
|
||||
"""测试检查用户名存在-返回false"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.check_username_exists("nonexistent_user")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_email_exists_true(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试检查邮箱存在-返回true"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.check_email_exists(test_user_data["email"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_email_exists_false(self, authenticated_client):
|
||||
"""测试检查邮箱存在-返回false"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.check_email_exists("nonexistent@example.com")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_by_page_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试分页获取用户成功"""
|
||||
import time
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
for i in range(5):
|
||||
user_data = test_user_data.copy()
|
||||
user_data["username"] = f"testuser_{timestamp}_{i}"
|
||||
user_data["email"] = f"testuser_{timestamp}_{i}@example.com"
|
||||
create_response = await user_api.create_user(user_data)
|
||||
cleanup_user.append(create_response.json()["id"])
|
||||
|
||||
response = await user_api.get_users_by_page(page=0, size=10)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "content" in data
|
||||
assert "totalElements" in data
|
||||
assert "totalPages" in data
|
||||
assert "currentPage" in data
|
||||
assert "pageSize" in data
|
||||
assert len(data["content"]) <= 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_by_page_with_sort(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试分页获取用户并排序成功"""
|
||||
import time
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
user1_data = test_user_data.copy()
|
||||
user1_data["username"] = f"user_a_{timestamp}"
|
||||
user1_data["email"] = f"user_a_{timestamp}@example.com"
|
||||
create_response1 = await user_api.create_user(user1_data)
|
||||
cleanup_user.append(create_response1.json()["id"])
|
||||
|
||||
user2_data = test_user_data.copy()
|
||||
user2_data["username"] = f"user_b_{timestamp}"
|
||||
user2_data["email"] = f"user_b_{timestamp}@example.com"
|
||||
create_response2 = await user_api.create_user(user2_data)
|
||||
cleanup_user.append(create_response2.json()["id"])
|
||||
|
||||
response = await user_api.get_users_by_page(page=0, size=10, sort="username", order="asc")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
usernames = [user["username"] for user in data["content"]]
|
||||
assert usernames == sorted(usernames)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_by_page_with_search(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试分页获取用户并搜索成功"""
|
||||
import time
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
user1_data = test_user_data.copy()
|
||||
user1_data["username"] = f"search_test_user_{timestamp}"
|
||||
user1_data["email"] = f"search_test_{timestamp}@example.com"
|
||||
create_response1 = await user_api.create_user(user1_data)
|
||||
cleanup_user.append(create_response1.json()["id"])
|
||||
|
||||
user2_data = test_user_data.copy()
|
||||
user2_data["username"] = f"other_user_{timestamp}"
|
||||
user2_data["email"] = f"other_{timestamp}@example.com"
|
||||
create_response2 = await user_api.create_user(user2_data)
|
||||
cleanup_user.append(create_response2.json()["id"])
|
||||
|
||||
response = await user_api.get_users_by_page(page=0, size=10, keyword="search")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["content"]) >= 1
|
||||
assert all("search" in user["username"] or "search" in user["email"]
|
||||
for user in data["content"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_count_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试获取用户总数成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
initial_count_response = await user_api.get_user_count()
|
||||
initial_count = initial_count_response.json()
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
cleanup_user.append(create_response.json()["id"])
|
||||
|
||||
final_count_response = await user_api.get_user_count()
|
||||
final_count = final_count_response.json()
|
||||
|
||||
assert final_count == initial_count + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_by_page_with_different_page_sizes(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试不同页面大小的分页获取用户成功"""
|
||||
import time
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
for i in range(15):
|
||||
user_data = test_user_data.copy()
|
||||
user_data["username"] = f"pagesize_test_{timestamp}_{i}"
|
||||
user_data["email"] = f"pagesize_test_{timestamp}_{i}@example.com"
|
||||
create_response = await user_api.create_user(user_data)
|
||||
cleanup_user.append(create_response.json()["id"])
|
||||
|
||||
response_size_10 = await user_api.get_users_by_page(page=0, size=10)
|
||||
assert response_size_10.status_code == 200
|
||||
data_size_10 = response_size_10.json()
|
||||
assert len(data_size_10["content"]) == 10
|
||||
|
||||
response_size_5 = await user_api.get_users_by_page(page=0, size=5)
|
||||
assert response_size_5.status_code == 200
|
||||
data_size_5 = response_size_5.json()
|
||||
assert len(data_size_5["content"]) == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_by_page_with_page_navigation(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试分页导航成功"""
|
||||
import time
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
timestamp = int(time.time() * 1000)
|
||||
for i in range(25):
|
||||
user_data = test_user_data.copy()
|
||||
user_data["username"] = f"pagination_test_{timestamp}_{i}"
|
||||
user_data["email"] = f"pagination_test_{timestamp}_{i}@example.com"
|
||||
create_response = await user_api.create_user(user_data)
|
||||
cleanup_user.append(create_response.json()["id"])
|
||||
|
||||
page1_response = await user_api.get_users_by_page(page=0, size=10)
|
||||
page1_data = page1_response.json()
|
||||
assert page1_data["currentPage"] == 0
|
||||
assert len(page1_data["content"]) == 10
|
||||
|
||||
page2_response = await user_api.get_users_by_page(page=1, size=10)
|
||||
page2_data = page2_response.json()
|
||||
assert page2_data["currentPage"] == 1
|
||||
assert len(page2_data["content"]) == 10
|
||||
|
||||
page3_response = await user_api.get_users_by_page(page=2, size=10)
|
||||
page3_data = page3_response.json()
|
||||
assert page3_data["currentPage"] == 2
|
||||
assert len(page3_data["content"]) >= 5
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
WebSocket实时通信测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from websockets.client import connect
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
|
||||
@pytest.mark.websocket
|
||||
@pytest.mark.regression
|
||||
class TestWebSocket:
|
||||
"""WebSocket实时通信测试类"""
|
||||
|
||||
@pytest.fixture
|
||||
def websocket_url(self):
|
||||
"""WebSocket连接URL"""
|
||||
api_base_url = os.getenv("API_BASE_URL", "http://localhost:8084")
|
||||
ws_url = api_base_url.replace("http://", "ws://")
|
||||
return f"{ws_url}/ws"
|
||||
|
||||
@pytest.fixture
|
||||
async def websocket_connection(self, websocket_url):
|
||||
"""WebSocket连接fixture"""
|
||||
async with connect(websocket_url) as websocket:
|
||||
yield websocket
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_websocket(self, websocket_url, authenticated_client):
|
||||
"""带认证的WebSocket连接"""
|
||||
token = authenticated_client.headers.get("Authorization")
|
||||
url_with_token = f"{websocket_url}?token={token.replace('Bearer ', '')}"
|
||||
|
||||
async with connect(url_with_token) as websocket:
|
||||
yield websocket
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_connection(self, websocket_url):
|
||||
"""测试WebSocket连接建立"""
|
||||
try:
|
||||
async with connect(websocket_url) as websocket:
|
||||
assert websocket.open
|
||||
except ConnectionRefusedError:
|
||||
pytest.skip("WebSocket服务未启动")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_ping_pong(self, websocket_connection):
|
||||
"""测试WebSocket心跳机制"""
|
||||
ping_message = {
|
||||
"type": "ping",
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
|
||||
await websocket_connection.send(json.dumps(ping_message))
|
||||
|
||||
response = await asyncio.wait_for(websocket_connection.recv(), timeout=5.0)
|
||||
pong_message = json.loads(response)
|
||||
|
||||
assert pong_message["type"] == "pong"
|
||||
assert "timestamp" in pong_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_subscribe(self, websocket_connection):
|
||||
"""测试WebSocket订阅"""
|
||||
subscribe_message = {
|
||||
"type": "subscribe",
|
||||
"channel": "notifications"
|
||||
}
|
||||
|
||||
await websocket_connection.send(json.dumps(subscribe_message))
|
||||
|
||||
response = await asyncio.wait_for(websocket_connection.recv(), timeout=5.0)
|
||||
message = json.loads(response)
|
||||
|
||||
assert message["type"] in ["subscribe_ack", "pong"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_multiple_messages(self, websocket_connection):
|
||||
"""测试WebSocket多消息处理"""
|
||||
messages = [
|
||||
{"type": "ping"},
|
||||
{"type": "subscribe", "channel": "test"},
|
||||
{"type": "ping"}
|
||||
]
|
||||
|
||||
responses = []
|
||||
for msg in messages:
|
||||
await websocket_connection.send(json.dumps(msg))
|
||||
try:
|
||||
response = await asyncio.wait_for(websocket_connection.recv(), timeout=2.0)
|
||||
responses.append(json.loads(response))
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
assert len(responses) >= 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_invalid_message(self, websocket_connection):
|
||||
"""测试WebSocket无效消息处理"""
|
||||
invalid_messages = [
|
||||
"invalid json",
|
||||
"",
|
||||
json.dumps({"type": "unknown_type"}),
|
||||
json.dumps({})
|
||||
]
|
||||
|
||||
for msg in invalid_messages:
|
||||
try:
|
||||
await websocket_connection.send(msg)
|
||||
await asyncio.sleep(0.5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_connection_close(self, websocket_url):
|
||||
"""测试WebSocket连接关闭"""
|
||||
async with connect(websocket_url) as websocket:
|
||||
assert websocket.open
|
||||
await websocket.close()
|
||||
assert not websocket.open
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_timeout(self, websocket_url):
|
||||
"""测试WebSocket超时"""
|
||||
try:
|
||||
async with connect(websocket_url, ping_timeout=2.0) as websocket:
|
||||
await asyncio.sleep(3.0)
|
||||
except (ConnectionClosed, asyncio.TimeoutError):
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_concurrent_connections(self, websocket_url):
|
||||
"""测试WebSocket并发连接"""
|
||||
async def create_connection():
|
||||
try:
|
||||
async with connect(websocket_url) as websocket:
|
||||
await websocket.send(json.dumps({"type": "ping"}))
|
||||
await asyncio.wait_for(websocket_connection.recv(), timeout=2.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
connections = [create_connection() for _ in range(5)]
|
||||
await asyncio.gather(*connections, return_exceptions=True)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_large_message(self, websocket_connection):
|
||||
"""测试WebSocket大消息处理"""
|
||||
large_data = "x" * 10000
|
||||
message = {
|
||||
"type": "test",
|
||||
"data": large_data
|
||||
}
|
||||
|
||||
await websocket_connection.send(json.dumps(message))
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(websocket_connection.recv(), timeout=5.0)
|
||||
assert response
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_reconnect(self, websocket_url):
|
||||
"""测试WebSocket重连"""
|
||||
for i in range(3):
|
||||
try:
|
||||
async with connect(websocket_url) as websocket:
|
||||
await websocket.send(json.dumps({"type": "ping"}))
|
||||
response = await asyncio.wait_for(websocket.recv(), timeout=2.0)
|
||||
assert response
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_unicode_message(self, websocket_connection):
|
||||
"""测试WebSocket Unicode消息"""
|
||||
unicode_message = {
|
||||
"type": "test",
|
||||
"content": "测试中文🎉🚀"
|
||||
}
|
||||
|
||||
await websocket_connection.send(json.dumps(unicode_message))
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(websocket_connection.recv(), timeout=2.0)
|
||||
assert response
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
@@ -0,0 +1,58 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8084';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
baseline: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
startTime: '0s',
|
||||
},
|
||||
stress_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '2m', target: 100 },
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '1m', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
spike_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 },
|
||||
{ duration: '10s', target: 200 },
|
||||
{ duration: '30s', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"scenarios": {
|
||||
"baseline": {
|
||||
"executor": "constant-vus",
|
||||
"vus": 10,
|
||||
"duration": "30s"
|
||||
},
|
||||
"stress_test": {
|
||||
"executor": "ramping-vus",
|
||||
"startVUs": 10,
|
||||
"stages": [
|
||||
{ "duration": "1m", "target": 50 },
|
||||
{ "duration": "2m", "target": 100 },
|
||||
{ "duration": "1m", "target": 50 },
|
||||
{ "duration": "1m", "target": 10 }
|
||||
]
|
||||
},
|
||||
"spike_test": {
|
||||
"executor": "ramping-vus",
|
||||
"startVUs": 10,
|
||||
"stages": [
|
||||
{ "duration": "30s", "target": 10 },
|
||||
{ "duration": "10s", "target": 200 },
|
||||
{ "duration": "30s", "target": 10 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"http_req_duration": [
|
||||
{ "target": "p(95)<500", "abortOnFail": true }
|
||||
],
|
||||
"http_req_failed": [
|
||||
{ "target": "rate<0.05", "abortOnFail": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export function setup() {
|
||||
let loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
check(loginRes, {
|
||||
'login successful': (r) => r.status === 200,
|
||||
'has token': (r) => r.json('token') !== undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
token: loginRes.json('token'),
|
||||
};
|
||||
}
|
||||
|
||||
export default function (data) {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${data.token}`,
|
||||
};
|
||||
|
||||
let responses = http.batch([
|
||||
['GET', `${BASE_URL}/api/users`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/roles`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/config`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/notices`, null, { headers }],
|
||||
['GET', `${BASE_URL}/api/files`, null, { headers }],
|
||||
]);
|
||||
|
||||
responses.forEach((res) => {
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown(data) {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let responses = http.batch([
|
||||
['GET', `${BASE_URL}/actuator/health`, null, null],
|
||||
['GET', `${BASE_URL}/actuator/info`, null, null],
|
||||
]);
|
||||
|
||||
responses.forEach((res) => {
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
baseline: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
startTime: '0s',
|
||||
},
|
||||
stress_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '2m', target: 100 },
|
||||
{ duration: '1m', target: 50 },
|
||||
{ duration: '1m', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
spike_test: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 10,
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 },
|
||||
{ duration: '10s', target: 200 },
|
||||
{ duration: '30s', target: 10 }
|
||||
],
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const TEST_DURATION = __ENV.DURATION || '30s';
|
||||
const VUS = __ENV.VUS || '10';
|
||||
|
||||
export let options = {
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: 'constant-vus',
|
||||
vus: parseInt(VUS),
|
||||
duration: TEST_DURATION,
|
||||
startTime: '0s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
let response = http.get(`${BASE_URL}/actuator/health`);
|
||||
|
||||
check(response, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has UP status': (r) => r.json('status') === 'UP',
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function teardown() {
|
||||
console.log('Performance test completed');
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
性能测试基础框架
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
import asyncio
|
||||
import statistics
|
||||
from typing import List, Dict, Any
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class PerformanceTest:
|
||||
"""性能测试基类"""
|
||||
|
||||
@pytest.fixture
|
||||
async def perf_client(self, authenticated_client: AsyncClient) -> AsyncClient:
|
||||
"""性能测试客户端"""
|
||||
return authenticated_client
|
||||
|
||||
@pytest.fixture
|
||||
def performance_thresholds(self):
|
||||
"""性能阈值配置"""
|
||||
return {
|
||||
"response_time_p95": 2000, # 95%的请求响应时间应小于2秒
|
||||
"response_time_p99": 5000, # 99%的请求响应时间应小于5秒
|
||||
"error_rate": 0.05, # 错误率应小于5%
|
||||
"throughput_min": 10, # 最小吞吐量(请求/秒)
|
||||
}
|
||||
|
||||
async def measure_request_time(self, client: AsyncClient, method: str,
|
||||
url: str, **kwargs) -> float:
|
||||
"""测量单个请求时间"""
|
||||
start_time = time.time()
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = await client.get(url, **kwargs)
|
||||
elif method.upper() == "POST":
|
||||
response = await client.post(url, **kwargs)
|
||||
elif method.upper() == "PUT":
|
||||
response = await client.put(url, **kwargs)
|
||||
elif method.upper() == "DELETE":
|
||||
response = await client.delete(url, **kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
end_time = time.time()
|
||||
response_time = (end_time - start_time) * 1000 # 转换为毫秒
|
||||
|
||||
return response_time
|
||||
|
||||
async def measure_concurrent_requests(self, client: AsyncClient, method: str,
|
||||
url: str, concurrency: int = 10,
|
||||
**kwargs) -> Dict[str, Any]:
|
||||
"""测量并发请求性能"""
|
||||
async def make_request():
|
||||
return await self.measure_request_time(client, method, url, **kwargs)
|
||||
|
||||
start_time = time.time()
|
||||
results = await asyncio.gather(*[make_request() for _ in range(concurrency)])
|
||||
end_time = time.time()
|
||||
|
||||
total_time = (end_time - start_time) * 1000 # 毫秒
|
||||
response_times = results
|
||||
|
||||
return {
|
||||
"concurrency": concurrency,
|
||||
"total_time_ms": total_time,
|
||||
"response_times_ms": response_times,
|
||||
"min_time_ms": min(response_times),
|
||||
"max_time_ms": max(response_times),
|
||||
"avg_time_ms": statistics.mean(response_times),
|
||||
"median_time_ms": statistics.median(response_times),
|
||||
"p95_time_ms": self._percentile(response_times, 95),
|
||||
"p99_time_ms": self._percentile(response_times, 99),
|
||||
"throughput_rps": concurrency / (total_time / 1000),
|
||||
"success_count": len(response_times),
|
||||
}
|
||||
|
||||
def _percentile(self, data: List[float], percentile: float) -> float:
|
||||
"""计算百分位数"""
|
||||
sorted_data = sorted(data)
|
||||
index = int(len(sorted_data) * percentile / 100)
|
||||
return sorted_data[min(index, len(sorted_data) - 1)]
|
||||
|
||||
def assert_performance(self, results: Dict[str, Any], thresholds: Dict[str, Any]):
|
||||
"""断言性能指标"""
|
||||
p95_time = results["p95_time_ms"]
|
||||
p99_time = results["p99_time_ms"]
|
||||
throughput = results["throughput_rps"]
|
||||
|
||||
if p95_time > thresholds["response_time_p95"]:
|
||||
pytest.fail(f"P95响应时间 {p95_time:.2f}ms 超过阈值 {thresholds['response_time_p95']}ms")
|
||||
|
||||
if p99_time > thresholds["response_time_p99"]:
|
||||
pytest.fail(f"P99响应时间 {p99_time:.2f}ms 超过阈值 {thresholds['response_time_p99']}ms")
|
||||
|
||||
if throughput < thresholds["throughput_min"]:
|
||||
pytest.fail(f"吞吐量 {throughput:.2f} rps 低于最小值 {thresholds['throughput_min']} rps")
|
||||
|
||||
logger.info(f"性能测试通过: P95={p95_time:.2f}ms, P99={p99_time:.2f}ms, 吞吐量={throughput:.2f} rps")
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class TestAPIPerformance(PerformanceTest):
|
||||
"""API性能测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试用户列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"用户列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试角色列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/roles", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"角色列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notice_list_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试通知列表API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/notices", concurrency=20
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"通知列表API性能: {results}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_performance(self, perf_client: AsyncClient, performance_thresholds):
|
||||
"""测试搜索API性能"""
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users/page?keyword=test", concurrency=15
|
||||
)
|
||||
|
||||
self.assert_performance(results, performance_thresholds)
|
||||
logger.info(f"搜索API性能: {results}")
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.slow
|
||||
class TestLoadTesting(PerformanceTest):
|
||||
"""负载测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sustained_load(self, perf_client: AsyncClient):
|
||||
"""测试持续负载"""
|
||||
duration_seconds = 30
|
||||
requests_per_second = 5
|
||||
total_requests = duration_seconds * requests_per_second
|
||||
|
||||
response_times = []
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(total_requests):
|
||||
response_time = await self.measure_request_time(
|
||||
perf_client, "GET", "/api/users"
|
||||
)
|
||||
response_times.append(response_time)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed < duration_seconds:
|
||||
sleep_time = max(0, (i + 1) / requests_per_second - elapsed)
|
||||
await asyncio.sleep(max(0, sleep_time))
|
||||
|
||||
avg_time = statistics.mean(response_times)
|
||||
p95_time = self._percentile(response_times, 95)
|
||||
|
||||
logger.info(f"持续负载测试 - 平均响应时间: {avg_time:.2f}ms, P95: {p95_time:.2f}ms")
|
||||
|
||||
assert avg_time < 3000, f"平均响应时间 {avg_time:.2f}ms 超过阈值 3000ms"
|
||||
assert p95_time < 5000, f"P95响应时间 {p95_time:.2f}ms 超过阈值 5000ms"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spike_load(self, perf_client: AsyncClient):
|
||||
"""测试突发负载"""
|
||||
spike_sizes = [10, 50, 100, 50, 10]
|
||||
|
||||
for spike_size in spike_sizes:
|
||||
results = await self.measure_concurrent_requests(
|
||||
perf_client, "GET", "/api/users", concurrency=spike_size
|
||||
)
|
||||
|
||||
logger.info(f"突发负载测试 (并发={spike_size}): P95={results['p95_time_ms']:.2f}ms")
|
||||
|
||||
assert results["p95_time_ms"] < 10000, \
|
||||
f"突发负载 {spike_size} 并发时 P95响应时间超时"
|
||||
Reference in New Issue
Block a user