feat: 重构测试框架并优化代码结构

refactor(tests): 将e2e_tests迁移到tests_suite和api_integration_tests
style: 为Java类添加文档注释
docs: 更新.gitignore和配置文件
test: 添加性能测试和Playwright测试脚本
chore: 清理旧测试文件和配置
This commit is contained in:
张翔
2026-03-14 13:49:39 +08:00
parent 9e187f42e5
commit c50ccd258f
178 changed files with 8655 additions and 2519 deletions
+22
View File
@@ -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
+50
View File
@@ -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/
+522
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
from .settings import settings, get_settings, Settings, DevSettings, StagingSettings, ProductionSettings
__all__ = [
"settings",
"get_settings",
"Settings",
"DevSettings",
"StagingSettings",
"ProductionSettings",
]
+57
View File
@@ -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()
+237
View File
@@ -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()
View File
+47
View File
@@ -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
+58
View File
@@ -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()
}
+36
View File
@@ -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()
+26
View File
@@ -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
+22
View File
@@ -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'] },
},
],
});
+112
View File
@@ -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
+39
View File
@@ -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
+37
View File
@@ -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
View File
+3
View File
@@ -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")
+83
View File
@@ -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()
+72
View File
@@ -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
])
+33
View File
@@ -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()
+204
View File
@@ -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
])
View File
View File
+311
View File
@@ -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)
+50
View File
@@ -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(/登录/);
});
});
+47
View File
@@ -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');
});
});
+292
View File
@@ -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');
}
+36
View File
@@ -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响应时间超时"