feat: 完成sys模块迁移,对齐PHP/Java框架

- 重构sys模块架构,严格按admin/api/core分层
- 对齐所有sys实体与数据库表结构
- 实现完整的adminapi控制器,匹配PHP/Java契约
- 修复依赖注入问题,确保服务正确注册
- 添加自动迁移工具和契约验证
- 完善多租户支持和审计功能
- 统一命名规范,与PHP业务逻辑保持一致
This commit is contained in:
万物街
2025-09-21 21:29:28 +08:00
parent 2e361795d9
commit 127a4db1e3
839 changed files with 24932 additions and 57988 deletions

116
wwjcloud/test/e2e/README.md Normal file
View File

@@ -0,0 +1,116 @@
# E2E 测试脚本
本目录包含端到端End-to-End测试脚本用于验证WWJCloud NestJS应用的完整功能。
## 📋 测试脚本说明
### 1. 模块功能测试
#### `modules-test.ps1` (PowerShell版本)
- **用途**: 测试4个核心模块的API接口
- **测试模块**: Admin、Member、RBAC、Auth
- **运行环境**: Windows PowerShell
- **运行方式**: `.\modules-test.ps1`
#### `modules-test.sh` (Bash版本)
- **用途**: 测试4个核心模块的API接口
- **测试模块**: Admin、Member、RBAC、Auth
- **运行环境**: Linux/macOS Bash
- **运行方式**: `./modules-test.sh`
### 2. 配置中心测试
#### `config-center-test.ps1`
- **用途**: 测试配置中心功能
- **测试功能**:
- 系统配置获取
- 动态配置管理
- 配置验证
- 配置统计
- 动态配置创建
- **运行环境**: Windows PowerShell
- **运行方式**: `.\config-center-test.ps1`
## 🚀 使用方法
### 前置条件
1. 确保WWJCloud NestJS应用正在运行默认端口3000
2. 确保数据库连接正常
3. 确保有测试用的管理员账号(默认: admin/123456
### 运行测试
#### Windows环境
```powershell
# 进入测试目录
cd test\e2e
# 运行模块测试
.\modules-test.ps1
# 运行配置中心测试
.\config-center-test.ps1
```
#### Linux/macOS环境
```bash
# 进入测试目录
cd test/e2e
# 给脚本执行权限
chmod +x modules-test.sh
# 运行模块测试
./modules-test.sh
```
## 📊 测试覆盖范围
### 核心模块测试
-**基础连接测试**: 验证应用是否正常启动
-**Swagger文档测试**: 验证API文档是否可访问
-**Admin模块**: 管理员CRUD操作
-**Member模块**: 会员CRUD操作
-**RBAC模块**: 角色权限管理
-**Auth模块**: 认证授权功能
-**认证接口测试**: 需要token的接口
### 配置中心测试
-**登录认证**: 获取访问令牌
-**系统配置**: 获取系统配置信息
-**动态配置**: 配置的增删改查
-**配置验证**: 配置有效性检查
-**配置统计**: 配置使用统计
## 🔧 测试配置
### 默认配置
- **应用地址**: `http://localhost:3000`
- **管理员账号**: `admin`
- **管理员密码**: `123456`
### 自定义配置
可以修改脚本中的以下变量来适应不同环境:
- `$BaseUrl` / `BASE_URL`: 应用访问地址
- 登录凭据: 根据实际环境调整
## 📝 测试结果
测试脚本会输出彩色的测试结果:
-**绿色**: 测试通过
-**红色**: 测试失败
- **蓝色**: 信息提示
## 🛠️ 维护说明
这些测试脚本应该:
1. **定期更新**: 随着API接口的变化及时更新
2. **持续集成**: 可集成到CI/CD流程中
3. **环境适配**: 支持不同的部署环境
4. **错误处理**: 提供详细的错误信息和调试帮助
## 📚 相关文档
- [API接口文档](../../docs/API_INTERFACE_COMPARISON.md)
- [认证授权指南](../../docs/AUTHENTICATION_GUIDE.md)
- [配置设置指南](../../docs/CONFIG_SETUP.md)

View File

@@ -0,0 +1,102 @@
# 配置中心测试脚本
Write-Host "=== 配置中心功能测试 ===" -ForegroundColor Green
# 1. 登录获取令牌
Write-Host "1. 登录获取令牌..." -ForegroundColor Yellow
$loginBody = @{
username = "admin"
password = "123456"
} | ConvertTo-Json
try {
$loginResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/auth/login" -Method POST -ContentType "application/json" -Body $loginBody
$loginData = $loginResponse.Content | ConvertFrom-Json
if ($loginData.token) {
$token = $loginData.token
Write-Host "✓ 登录成功,获取到令牌" -ForegroundColor Green
# 2. 测试系统配置接口
Write-Host "`n2. 测试系统配置接口..." -ForegroundColor Yellow
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
try {
$systemConfigResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/config/system" -Method GET -Headers $headers
$systemConfig = $systemConfigResponse.Content | ConvertFrom-Json
Write-Host "✓ 系统配置获取成功" -ForegroundColor Green
Write-Host "配置内容: $($systemConfig | ConvertTo-Json -Depth 2)" -ForegroundColor Cyan
}
catch {
Write-Host "✗ 系统配置获取失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 3. 测试动态配置列表
Write-Host "`n3. 测试动态配置列表..." -ForegroundColor Yellow
try {
$dynamicConfigResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/config/dynamic" -Method GET -Headers $headers
$dynamicConfig = $dynamicConfigResponse.Content | ConvertFrom-Json
Write-Host "✓ 动态配置列表获取成功" -ForegroundColor Green
Write-Host "动态配置: $($dynamicConfig | ConvertTo-Json)" -ForegroundColor Cyan
}
catch {
Write-Host "✗ 动态配置列表获取失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 4. 测试配置验证
Write-Host "`n4. 测试配置验证..." -ForegroundColor Yellow
try {
$validateResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/config/validate" -Method GET -Headers $headers
$validateResult = $validateResponse.Content | ConvertFrom-Json
Write-Host "✓ 配置验证成功" -ForegroundColor Green
Write-Host "验证结果: $($validateResult | ConvertTo-Json)" -ForegroundColor Cyan
}
catch {
Write-Host "✗ 配置验证失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 5. 测试配置统计
Write-Host "`n5. 测试配置统计..." -ForegroundColor Yellow
try {
$statsResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/config/stats" -Method GET -Headers $headers
$stats = $statsResponse.Content | ConvertFrom-Json
Write-Host "✓ 配置统计获取成功" -ForegroundColor Green
Write-Host "统计信息: $($stats | ConvertTo-Json)" -ForegroundColor Cyan
}
catch {
Write-Host "✗ 配置统计获取失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 6. 测试创建动态配置
Write-Host "`n6. 测试创建动态配置..." -ForegroundColor Yellow
$newConfigBody = @{
key = "test.feature.flag"
value = $true
description = "测试功能开关"
type = "boolean"
category = "test"
isPublic = $true
} | ConvertTo-Json
try {
$createResponse = Invoke-WebRequest -Uri "http://localhost:3000/adminapi/config/dynamic" -Method POST -Headers $headers -Body $newConfigBody
$createResult = $createResponse.Content | ConvertFrom-Json
Write-Host "✓ 动态配置创建成功" -ForegroundColor Green
Write-Host "创建结果: $($createResult | ConvertTo-Json)" -ForegroundColor Cyan
}
catch {
Write-Host "✗ 动态配置创建失败: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
Write-Host "✗ 登录失败,未获取到令牌" -ForegroundColor Red
}
}
catch {
Write-Host "✗ 登录请求失败: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n=== 测试完成 ===" -ForegroundColor Green

View File

@@ -0,0 +1,300 @@
# WWJ Cloud 模块测试脚本 (PowerShell版本)
# 测试4个核心模块的API接口
$BaseUrl = "http://localhost:3000"
$AdminToken = ""
$MemberToken = ""
Write-Host "🚀 开始测试WWJ Cloud核心模块..." -ForegroundColor Cyan
# 颜色输出函数
function Write-Success {
param([string]$Message)
Write-Host "$Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host "$Message" -ForegroundColor Red
}
function Write-Info {
param([string]$Message)
Write-Host " $Message" -ForegroundColor Blue
}
# 测试基础连接
function Test-Connection {
Write-Info "测试应用连接..."
try {
$response = Invoke-WebRequest -Uri $BaseUrl -Method GET -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Success "应用连接成功"
} else {
Write-Error "应用连接失败: HTTP $($response.StatusCode)"
exit 1
}
} catch {
Write-Error "应用连接失败: $($_.Exception.Message)"
exit 1
}
}
# 测试Swagger文档
function Test-Swagger {
Write-Info "测试Swagger文档..."
# 测试主API文档
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/api" -Method GET -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Success "主API文档可访问"
} else {
Write-Error "主API文档访问失败: HTTP $($response.StatusCode)"
}
} catch {
Write-Error "主API文档访问失败: $($_.Exception.Message)"
}
# 测试管理API文档
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/api/admin" -Method GET -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Success "管理API文档可访问"
} else {
Write-Error "管理API文档访问失败: HTTP $($response.StatusCode)"
}
} catch {
Write-Error "管理API文档访问失败: $($_.Exception.Message)"
}
}
# 测试Admin模块
function Test-AdminModule {
Write-Info "测试Admin模块..."
# 创建测试管理员
$adminData = @{
username = "testadmin"
password = "123456"
real_name = "测试管理员"
status = 1
site_id = 0
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/admin" -Method POST -Body $adminData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "uid") {
Write-Success "创建管理员成功"
} else {
Write-Error "创建管理员失败: $($response.Content)"
}
} catch {
Write-Error "创建管理员失败: $($_.Exception.Message)"
}
# 获取管理员列表
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/admin?page=1&limit=10" -Method GET -UseBasicParsing
if ($response.Content -match "data") {
Write-Success "获取管理员列表成功"
} else {
Write-Error "获取管理员列表失败: $($response.Content)"
}
} catch {
Write-Error "获取管理员列表失败: $($_.Exception.Message)"
}
}
# 测试Member模块
function Test-MemberModule {
Write-Info "测试Member模块..."
# 创建测试会员
$memberData = @{
username = "testmember"
password = "123456"
nickname = "测试会员"
mobile = "13800138000"
email = "test@example.com"
status = 1
site_id = 0
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/member" -Method POST -Body $memberData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "member_id") {
Write-Success "创建会员成功"
} else {
Write-Error "创建会员失败: $($response.Content)"
}
} catch {
Write-Error "创建会员失败: $($_.Exception.Message)"
}
# 获取会员列表
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/member?page=1&limit=10" -Method GET -UseBasicParsing
if ($response.Content -match "data") {
Write-Success "获取会员列表成功"
} else {
Write-Error "获取会员列表失败: $($response.Content)"
}
} catch {
Write-Error "获取会员列表失败: $($_.Exception.Message)"
}
}
# 测试RBAC模块
function Test-RbacModule {
Write-Info "测试RBAC模块..."
# 创建测试角色
$roleData = @{
roleName = "测试角色"
roleDesc = "测试角色描述"
status = 1
appType = "admin"
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/role" -Method POST -Body $roleData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "roleId") {
Write-Success "创建角色成功"
} else {
Write-Error "创建角色失败: $($response.Content)"
}
} catch {
Write-Error "创建角色失败: $($_.Exception.Message)"
}
# 创建测试菜单
$menuData = @{
menuName = "测试菜单"
menuType = 1
status = 1
appType = "admin"
path = "/test"
sort = 1
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/menu" -Method POST -Body $menuData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "menuId") {
Write-Success "创建菜单成功"
} else {
Write-Error "创建菜单失败: $($response.Content)"
}
} catch {
Write-Error "创建菜单失败: $($_.Exception.Message)"
}
}
# 测试Auth模块
function Test-AuthModule {
Write-Info "测试Auth模块..."
# 测试管理员登录
$adminLoginData = @{
username = "admin"
password = "123456"
siteId = 0
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/auth/admin/login" -Method POST -Body $adminLoginData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "accessToken") {
Write-Success "管理员登录成功"
# 提取token用于后续测试
$script:AdminToken = ($response.Content | ConvertFrom-Json).accessToken
} else {
Write-Error "管理员登录失败: $($response.Content)"
}
} catch {
Write-Error "管理员登录失败: $($_.Exception.Message)"
}
# 测试会员登录
$memberLoginData = @{
username = "member"
password = "123456"
siteId = 0
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/auth/member/login" -Method POST -Body $memberLoginData -ContentType "application/json" -UseBasicParsing
if ($response.Content -match "accessToken") {
Write-Success "会员登录成功"
# 提取token用于后续测试
$script:MemberToken = ($response.Content | ConvertFrom-Json).accessToken
} else {
Write-Error "会员登录失败: $($response.Content)"
}
} catch {
Write-Error "会员登录失败: $($_.Exception.Message)"
}
}
# 测试带认证的接口
function Test-AuthenticatedApis {
Write-Info "测试需要认证的接口..."
if ($AdminToken) {
# 测试获取管理员统计信息
$headers = @{
"Authorization" = "Bearer $AdminToken"
}
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/adminapi/admin/stats/overview" -Method GET -Headers $headers -UseBasicParsing
if ($response.Content -match "total") {
Write-Success "获取管理员统计信息成功"
} else {
Write-Error "获取管理员统计信息失败: $($response.Content)"
}
} catch {
Write-Error "获取管理员统计信息失败: $($_.Exception.Message)"
}
}
if ($MemberToken) {
# 测试获取会员信息
$headers = @{
"Authorization" = "Bearer $MemberToken"
}
try {
$response = Invoke-WebRequest -Uri "$BaseUrl/auth/profile" -Method GET -Headers $headers -UseBasicParsing
if ($response.Content -match "userId") {
Write-Success "获取会员信息成功"
} else {
Write-Error "获取会员信息失败: $($response.Content)"
}
} catch {
Write-Error "获取会员信息失败: $($_.Exception.Message)"
}
}
}
# 主测试流程
function Main {
Write-Host "==========================================" -ForegroundColor Yellow
Write-Host "WWJ Cloud 核心模块测试" -ForegroundColor Yellow
Write-Host "==========================================" -ForegroundColor Yellow
Test-Connection
Test-Swagger
Test-AdminModule
Test-MemberModule
Test-RbacModule
Test-AuthModule
Test-AuthenticatedApis
Write-Host "==========================================" -ForegroundColor Yellow
Write-Success "所有模块测试完成!"
Write-Host "==========================================" -ForegroundColor Yellow
}
# 运行测试
Main

View File

@@ -0,0 +1,248 @@
#!/bin/bash
# WWJ Cloud 模块测试脚本
# 测试4个核心模块的API接口
BASE_URL="http://localhost:3000"
ADMIN_TOKEN=""
MEMBER_TOKEN=""
echo "🚀 开始测试WWJ Cloud核心模块..."
# 颜色输出函数
print_success() {
echo -e "\033[32m✅ $1\033[0m"
}
print_error() {
echo -e "\033[31m❌ $1\033[0m"
}
print_info() {
echo -e "\033[34m $1\033[0m"
}
# 测试基础连接
test_connection() {
print_info "测试应用连接..."
response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL")
if [ "$response" = "200" ]; then
print_success "应用连接成功"
else
print_error "应用连接失败: HTTP $response"
exit 1
fi
}
# 测试Swagger文档
test_swagger() {
print_info "测试Swagger文档..."
# 测试主API文档
response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api")
if [ "$response" = "200" ]; then
print_success "主API文档可访问"
else
print_error "主API文档访问失败: HTTP $response"
fi
# 测试管理API文档
response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/admin")
if [ "$response" = "200" ]; then
print_success "管理API文档可访问"
else
print_error "管理API文档访问失败: HTTP $response"
fi
}
# 测试Admin模块
test_admin_module() {
print_info "测试Admin模块..."
# 创建测试管理员
response=$(curl -s -X POST "$BASE_URL/adminapi/admin" \
-H "Content-Type: application/json" \
-d '{
"username": "testadmin",
"password": "123456",
"real_name": "测试管理员",
"status": 1,
"site_id": 0
}')
if echo "$response" | grep -q "uid"; then
print_success "创建管理员成功"
else
print_error "创建管理员失败: $response"
fi
# 获取管理员列表
response=$(curl -s "$BASE_URL/adminapi/admin?page=1&limit=10")
if echo "$response" | grep -q "data"; then
print_success "获取管理员列表成功"
else
print_error "获取管理员列表失败: $response"
fi
}
# 测试Member模块
test_member_module() {
print_info "测试Member模块..."
# 创建测试会员
response=$(curl -s -X POST "$BASE_URL/adminapi/member" \
-H "Content-Type: application/json" \
-d '{
"username": "testmember",
"password": "123456",
"nickname": "测试会员",
"mobile": "13800138000",
"email": "test@example.com",
"status": 1,
"site_id": 0
}')
if echo "$response" | grep -q "member_id"; then
print_success "创建会员成功"
else
print_error "创建会员失败: $response"
fi
# 获取会员列表
response=$(curl -s "$BASE_URL/adminapi/member?page=1&limit=10")
if echo "$response" | grep -q "data"; then
print_success "获取会员列表成功"
else
print_error "获取会员列表失败: $response"
fi
}
# 测试RBAC模块
test_rbac_module() {
print_info "测试RBAC模块..."
# 创建测试角色
response=$(curl -s -X POST "$BASE_URL/adminapi/role" \
-H "Content-Type: application/json" \
-d '{
"roleName": "测试角色",
"roleDesc": "测试角色描述",
"status": 1,
"appType": "admin"
}')
if echo "$response" | grep -q "roleId"; then
print_success "创建角色成功"
else
print_error "创建角色失败: $response"
fi
# 创建测试菜单
response=$(curl -s -X POST "$BASE_URL/adminapi/menu" \
-H "Content-Type: application/json" \
-d '{
"menuName": "测试菜单",
"menuType": 1,
"status": 1,
"appType": "admin",
"path": "/test",
"sort": 1
}')
if echo "$response" | grep -q "menuId"; then
print_success "创建菜单成功"
else
print_error "创建菜单失败: $response"
fi
}
# 测试Auth模块
test_auth_module() {
print_info "测试Auth模块..."
# 测试管理员登录
response=$(curl -s -X POST "$BASE_URL/auth/admin/login" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "123456",
"siteId": 0
}')
if echo "$response" | grep -q "accessToken"; then
print_success "管理员登录成功"
# 提取token用于后续测试
ADMIN_TOKEN=$(echo "$response" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
else
print_error "管理员登录失败: $response"
fi
# 测试会员登录
response=$(curl -s -X POST "$BASE_URL/auth/member/login" \
-H "Content-Type: application/json" \
-d '{
"username": "member",
"password": "123456",
"siteId": 0
}')
if echo "$response" | grep -q "accessToken"; then
print_success "会员登录成功"
# 提取token用于后续测试
MEMBER_TOKEN=$(echo "$response" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
else
print_error "会员登录失败: $response"
fi
}
# 测试带认证的接口
test_authenticated_apis() {
print_info "测试需要认证的接口..."
if [ -n "$ADMIN_TOKEN" ]; then
# 测试获取管理员统计信息
response=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$BASE_URL/adminapi/admin/stats/overview")
if echo "$response" | grep -q "total"; then
print_success "获取管理员统计信息成功"
else
print_error "获取管理员统计信息失败: $response"
fi
fi
if [ -n "$MEMBER_TOKEN" ]; then
# 测试获取会员信息
response=$(curl -s -H "Authorization: Bearer $MEMBER_TOKEN" \
"$BASE_URL/auth/profile")
if echo "$response" | grep -q "userId"; then
print_success "获取会员信息成功"
else
print_error "获取会员信息失败: $response"
fi
fi
}
# 主测试流程
main() {
echo "=========================================="
echo "WWJ Cloud 核心模块测试"
echo "=========================================="
test_connection
test_swagger
test_admin_module
test_member_module
test_rbac_module
test_auth_module
test_authenticated_apis
echo "=========================================="
print_success "所有模块测试完成!"
echo "=========================================="
}
# 运行测试
main

View File

@@ -0,0 +1,225 @@
import { Controller, Get, Req, UseGuards, Module } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { SecurityModule } from '../../src/core/security/securityModule';
import { AdminCheckTokenGuard } from '../../src/core/security/adminCheckToken.guard';
import { ApiCheckTokenGuard } from '../../src/core/security/apiCheckToken.guard';
import { ApiOptionalAuthGuard } from '../../src/core/security/apiOptionalAuth.guard';
import { SiteScopeGuard } from '../../src/core/security/siteScopeGuard';
import { TokenAuthService } from '../../src/core/security/tokenAuth.service';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
@Controller()
class SecurityTestController {
// 管理端受保护路由(仅校验 token
@Get('adminapi/secure')
@UseGuards(AdminCheckTokenGuard)
adminSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 管理端受保护路由(校验 token + 站点隔离)
@Get('adminapi/secure-site')
@UseGuards(AdminCheckTokenGuard, SiteScopeGuard)
adminSecureSite(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台受保护路由(必须登录)
@Get('api/secure')
@UseGuards(ApiCheckTokenGuard)
apiSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
memberId: req.auth?.('member_id') ?? req.memberId ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台可选登录路由(未登录也可访问)
@Get('api/optional')
@UseGuards(ApiOptionalAuthGuard)
apiOptional(@Req() req: any) {
const hasUser = !!(req.auth?.('member_id') || req.memberId);
return {
appType: req.auth?.('app_type') || req.appType || '',
hasUser,
};
}
}
@Module({
imports: [SecurityModule, NestCacheModule.register()],
controllers: [SecurityTestController],
})
class SecurityTestModule {}
describe('Security Guards (e2e)', () => {
let app: any;
let tokenService: TokenAuthService;
// 内存版 Redis 替身(仅实现 get/set/del
const memory = new Map<string, string>();
const mockRedis = {
async get(key: string) {
return memory.has(key) ? memory.get(key)! : null;
},
async set(key: string, value: string) {
memory.set(key, value);
return 'OK';
},
async del(key: string) {
const existed = memory.delete(key);
return existed ? 1 : 0;
},
} as any;
// 轻量 cache-manager 替身,满足 CACHE_MANAGER 依赖
const mockCache = {
async get<T>(_key: string): Promise<T | null> {
return null;
},
async set<T>(_key: string, _value: T, _ttl?: number): Promise<void> {
return;
},
async del(_key: string): Promise<void> {
return;
},
} as any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [SecurityTestModule],
})
.overrideProvider('REDIS_CLIENT')
.useValue(mockRedis)
.overrideProvider(CACHE_MANAGER)
.useValue(mockCache)
.compile();
app = (await moduleFixture.createNestApplication()).getHttpServer
? await moduleFixture.createNestApplication()
: await moduleFixture.createNestApplication();
await app.init();
tokenService = moduleFixture.get(TokenAuthService);
});
afterAll(async () => {
if (app) {
await app.close();
}
});
it('Admin: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/adminapi/secure').expect(401);
});
it('Admin: 200 with valid token (no site header)', async () => {
const { token } = await tokenService.createToken(
1001,
'admin',
{
uid: 1001,
username: 'root',
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure')
.set('token', token)
.expect(200);
expect(res.body.data?.uid ?? res.body.uid).toBe(1001);
const appType = res.body.data?.appType ?? res.body.appType;
expect(appType).toBe('admin');
});
it('Admin: 403 on site mismatch with SiteScopeGuard', async () => {
const { token, params } = await tokenService.createToken(
2002,
'admin',
{
uid: 2002,
username: 'admin2',
site_id: 2,
},
600,
);
// header 指定不同 site-id 触发越权
await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '3')
.expect(403);
});
it('Admin: 200 when site matches with SiteScopeGuard', async () => {
const { token } = await tokenService.createToken(
2003,
'admin',
{
uid: 2003,
username: 'admin3',
site_id: 5,
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '5')
.expect(200);
expect(res.body.data?.siteId ?? res.body.siteId).toBe(5);
});
it('API: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/api/secure').expect(401);
});
it('API: 200 with valid token', async () => {
const { token } = await tokenService.createToken(
3001,
'api',
{
member_id: 3001,
username: 'member1',
site_id: 8,
},
600,
);
const res = await request(app.getHttpServer())
.get('/api/secure')
.set('token', token)
.expect(200);
const memberId = res.body.data?.memberId ?? res.body.memberId;
expect(memberId).toBe(3001);
});
it('API Optional: 200 without token and no user', async () => {
const res = await request(app.getHttpServer())
.get('/api/optional')
.expect(200);
const hasUser = res.body.data?.hasUser ?? res.body.hasUser;
expect(hasUser).toBe(false);
});
});

View File

@@ -0,0 +1,75 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../../../src/app.module';
describe('SYS Module e2e', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
// 租户隔离:前台按 siteId 查询配置
it('GET /api/config/:key should respect site isolation', async () => {
const siteId = 0; // TODO: 准备测试数据后替换
const res = await request(app.getHttpServer())
.get('/api/config/some_key')
.set('x-site-id', String(siteId))
.expect(200);
expect(res.body).toBeDefined();
});
// 权限校验:无角色访问受限接口应被拒绝
it('GET /adminapi/sys/menu/list without token should be unauthorized', async () => {
await request(app.getHttpServer())
.get('/adminapi/sys/menu/list')
.expect(401);
});
// 批量读取配置
it('GET /api/config?keys=a,b should return object', async () => {
const res = await request(app.getHttpServer())
.get('/api/config')
.query({ keys: 'a,b' })
.expect(200);
expect(typeof res.body).toBe('object');
});
// 其他管理端受限接口 401 验证
it('GET /adminapi/sys/dict/types without token should be unauthorized', async () => {
await request(app.getHttpServer())
.get('/adminapi/sys/dict/types')
.expect(401);
});
it('GET /adminapi/sys/area/list without token should be unauthorized', async () => {
await request(app.getHttpServer())
.get('/adminapi/sys/area/list')
.expect(401);
});
// 前台公开接口 200 验证
it('GET /api/dict/demo/items should return 200 (even if empty)', async () => {
const res = await request(app.getHttpServer())
.get('/api/dict/demo/items')
.expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('GET /api/area/tree should return 200 (even if empty)', async () => {
const res = await request(app.getHttpServer())
.get('/api/area/tree')
.expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
});

View File

@@ -0,0 +1,70 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { HealthzController } from '../src/core/health/healthzController';
import { HealthService } from '../src/core/health/healthService';
// 使用最小化测试应用,避免引入 AppModule 的外部依赖DB/Redis/Kafka 等)
describe('Health Endpoints (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
controllers: [HealthzController],
providers: [
{
provide: HealthService,
useValue: {
// 模拟健康检查,保证端点可用
check: jest.fn(async () => ({ status: 'ok' })),
detailedCheck: jest.fn(async () => [
{ name: 'database', status: 'up' },
{ name: 'queue', status: 'up' },
{ name: 'eventBus', status: 'up' },
{ name: 'cache', status: 'up' },
]),
checkDatabase: jest.fn(async () => ({ status: 'up' })),
},
},
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
if (app) await app.close();
});
it('GET /healthz should be public and return 200 or 503 with JSON body', async () => {
const res = await request(app.getHttpServer()).get('/healthz');
expect([200, 503]).toContain(res.status);
expect(res.headers['content-type']).toMatch(/json/);
expect(res.body).toHaveProperty('status');
});
it('GET /readyz should be public and return 200 or 503 with JSON body', async () => {
const res = await request(app.getHttpServer()).get('/readyz');
expect([200, 503]).toContain(res.status);
expect(res.headers['content-type']).toMatch(/json/);
expect(res.body).toHaveProperty('status');
});
it('GET /health/livez alias should respond', async () => {
const res = await request(app.getHttpServer()).get('/health/livez');
expect([200, 503]).toContain(res.status);
});
it('GET /health/readyz alias should respond', async () => {
const res = await request(app.getHttpServer()).get('/health/readyz');
expect([200, 503]).toContain(res.status);
});
it('GET /startupz should be public and return 200 or 503 with JSON body', async () => {
const res = await request(app.getHttpServer()).get('/startupz');
expect([200, 503]).toContain(res.status);
expect(res.headers['content-type']).toMatch(/json/);
expect(res.body).toHaveProperty('status');
});
});

View File

@@ -0,0 +1,174 @@
import { Injectable } from '@nestjs/common';
import type {
ITaskQueueProvider,
IEventBusProvider,
TaskJobOptions,
TaskProcessor,
ITaskQueue,
TaskJob,
} from '@wwjCore/interfaces/queue.interface';
import type {
DomainEvent,
EventHandler,
EventPublishOptions,
} from '@wwjCore/interfaces/eventInterface';
interface InternalQueueState {
jobs: TaskJob[];
paused: boolean;
}
let seq = 0;
function genId(prefix = 'job') {
seq += 1;
return `${prefix}_${Date.now()}_${seq}`;
}
@Injectable()
export class InMemoryQueueProvider
implements ITaskQueueProvider, IEventBusProvider
{
private processors = new Map<string, TaskProcessor<any>>();
private queues = new Map<string, InternalQueueState>();
private eventHandlers = new Map<string, EventHandler[]>();
getQueue(name: string): ITaskQueue {
return {
add: async (jobType: string, payload: any, options?: TaskJobOptions) =>
this.addJob(name, jobType, payload, options),
addJob: async (jobName: string, data: any, options?: TaskJobOptions) =>
this.addJob(name, jobName, data, options),
process: async (processor: TaskProcessor<any>) =>
this.process(name, processor),
getStats: async () => this.getQueueStatus(name),
pause: async () => this.pause(name),
resume: async () => this.resume(name),
close: async () => this.close(),
} as ITaskQueue;
}
private ensureQueue(name: string) {
if (!this.queues.has(name)) {
this.queues.set(name, { jobs: [], paused: false });
}
return this.queues.get(name)!;
}
async addJob<T = any>(
queueName: string,
jobName: string,
data: T,
options?: TaskJobOptions,
): Promise<void> {
const q = this.ensureQueue(queueName);
const job: TaskJob<T> = {
id: genId('job'),
type: jobName,
data,
attemptsMade: 0,
timestamp: Date.now(),
};
q.jobs.push(job);
const delay = options?.delay ?? 0;
const processor = this.processors.get(queueName);
if (processor && !q.paused) {
setTimeout(async () => {
try {
await processor(job);
} catch (e) {
// 忽略测试中的处理异常传播
}
}, delay);
}
}
async process<T = any>(
queueName: string,
processor: TaskProcessor<T>,
): Promise<void> {
const q = this.ensureQueue(queueName);
this.processors.set(queueName, processor);
// 处理已存在的积压任务
if (!q.paused && q.jobs.length > 0) {
for (const job of q.jobs) {
setTimeout(() => {
processor(job as TaskJob<T>).catch(() => void 0);
}, 0);
}
}
}
async getQueueStatus(queueName: string): Promise<any> {
const q = this.ensureQueue(queueName);
return {
name: queueName,
pending: q.jobs.length,
processing: this.processors.has(queueName) ? 1 : 0,
failed: 0,
paused: q.paused,
};
}
async pause(queueName: string): Promise<void> {
const q = this.ensureQueue(queueName);
q.paused = true;
}
async resume(queueName: string): Promise<void> {
const q = this.ensureQueue(queueName);
q.paused = false;
}
async healthCheck(): Promise<boolean> {
return true;
}
async close(): Promise<void> {
this.processors.clear();
this.queues.clear();
this.eventHandlers.clear();
}
// ========== 事件总线 ==========
async publish(
event: DomainEvent,
_options?: EventPublishOptions,
): Promise<void> {
const handlers = this.eventHandlers.get(event.eventType) || [];
await Promise.all(
handlers.map((h) =>
Promise.resolve()
.then(() => h(event))
.catch(() => void 0),
),
);
}
async publishBatch(
events: DomainEvent[],
options?: EventPublishOptions,
): Promise<void> {
for (const e of events) {
await this.publish(e, options);
}
}
async subscribe(
eventType: string,
handler: EventHandler,
_options?: any,
): Promise<void> {
if (!this.eventHandlers.has(eventType))
this.eventHandlers.set(eventType, []);
this.eventHandlers.get(eventType)!.push(handler);
}
async unsubscribe(eventType: string, handler: EventHandler): Promise<void> {
const list = this.eventHandlers.get(eventType) || [];
this.eventHandlers.set(
eventType,
list.filter((h) => h !== handler),
);
}
}

View File

@@ -2,13 +2,16 @@
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"testRegex": "(e2e|queue)/.*\\.e2e-spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1"
"^src/(.*)$": "<rootDir>/../src/$1",
"^@wwjConfig(.*)$": "<rootDir>/../src/config$1",
"^@wwjCore(.*)$": "<rootDir>/../src/core$1",
"^@wwjVendor(.*)$": "<rootDir>/../src/vendor$1"
},
"setupFilesAfterEnv": [],
"setupFilesAfterEnv": ["<rootDir>/setup-e2e.ts"],
"testTimeout": 30000
}

View File

@@ -4,18 +4,15 @@ import request from 'supertest';
import { TestModule } from '../test.module';
import { TestService } from '../test.service';
import { UnifiedQueueService } from '../../src/core/queue/unifiedQueueService';
import { DatabaseQueueProvider } from '../../src/core/queue/databaseQueueProvider';
import { QueueModule } from '../../src/core/queue/queueModule';
describe('Queue System (e2e)', () => {
let app: INestApplication;
let testService: TestService;
let unifiedQueueService: UnifiedQueueService;
let databaseQueueProvider: DatabaseQueueProvider;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TestModule, QueueModule],
imports: [TestModule],
}).compile();
app = moduleFixture.createNestApplication();
@@ -24,9 +21,6 @@ describe('Queue System (e2e)', () => {
testService = moduleFixture.get<TestService>(TestService);
unifiedQueueService =
moduleFixture.get<UnifiedQueueService>(UnifiedQueueService);
databaseQueueProvider = moduleFixture.get<DatabaseQueueProvider>(
DatabaseQueueProvider,
);
});
afterAll(async () => {
@@ -84,13 +78,18 @@ describe('Queue System (e2e)', () => {
});
it('should add task to queue', async () => {
const result = await unifiedQueueService.addTask('test-queue', {
data: { test: 'data' },
priority: 1,
delay: 0,
attempts: 3,
});
expect(result).toBeDefined();
await expect(
unifiedQueueService.addTask(
'test-queue',
'test-task',
{ test: 'data' },
{
priority: 1,
delay: 0,
attempts: 3,
},
),
).resolves.toBeUndefined();
});
it('should process task from queue', async () => {
@@ -102,13 +101,17 @@ describe('Queue System (e2e)', () => {
});
// Add a task to be processed
await unifiedQueueService.addTask('test-queue', {
data: { test: 'process-data' },
priority: 1,
});
await unifiedQueueService.addTask(
'test-queue',
'process-task',
{ test: 'process-data' },
{
priority: 1,
},
);
// Wait a bit for processing
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 300));
expect(processedData).toBeDefined();
});
@@ -132,53 +135,10 @@ describe('Queue System (e2e)', () => {
});
});
describe('DatabaseQueueProvider', () => {
it('should be defined', () => {
expect(databaseQueueProvider).toBeDefined();
});
it('should add job to database queue', async () => {
const jobData = {
type: 'test-job',
payload: { test: 'database-job' },
options: {
priority: 1,
delay: 0,
attempts: 3,
},
};
const result = await databaseQueueProvider.add(
'test-db-queue',
jobData.type,
jobData.payload,
jobData.options,
);
expect(result).toBeDefined();
});
it('should publish event to database', async () => {
const event = {
eventType: 'test.database.event',
aggregateId: 'db-test-123',
aggregateType: 'DatabaseTest',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'db-key-123',
traceId: 'db-trace-123',
data: { test: 'database-event-data' },
};
await expect(databaseQueueProvider.publish(event)).resolves.not.toThrow();
});
});
describe('Service Integration', () => {
it('should have all required services available', () => {
expect(testService).toBeDefined();
expect(unifiedQueueService).toBeDefined();
expect(databaseQueueProvider).toBeDefined();
});
});
@@ -188,11 +148,16 @@ describe('Queue System (e2e)', () => {
const taskData = { workflow: 'test', step: 1 };
// Add task
const taskResult = await unifiedQueueService.addTask('workflow-queue', {
data: taskData,
priority: 1,
});
expect(taskResult).toBeDefined();
await expect(
unifiedQueueService.addTask(
'workflow-queue',
'workflow-task',
taskData,
{
priority: 1,
},
),
).resolves.toBeUndefined();
// Process task and publish event
await unifiedQueueService.processTask(
@@ -216,7 +181,7 @@ describe('Queue System (e2e)', () => {
);
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 300));
});
it('should handle error scenarios gracefully', async () => {
@@ -229,14 +194,18 @@ describe('Queue System (e2e)', () => {
});
// Add a failing task
await unifiedQueueService.addTask('error-queue', {
data: { shouldFail: true },
priority: 1,
attempts: 1, // Only try once
});
await unifiedQueueService.addTask(
'error-queue',
'error',
{ shouldFail: true },
{
priority: 1,
attempts: 1, // Only try once
},
);
// Wait for processing attempt
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 300));
// The test passes if no unhandled errors are thrown
expect(true).toBe(true);

View File

@@ -0,0 +1,28 @@
import dotenv from 'dotenv';
dotenv.config();
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
// 关闭噪声日志,便于测试输出阅读
if (!process.env.LOG_LEVEL) {
process.env.LOG_LEVEL = 'warn';
}
// 为单测提供最小必需配置占位(不连接真实中间件)
process.env.REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1';
process.env.REDIS_PORT = process.env.REDIS_PORT || '6379';
// 满足配置校验的最小必需字段(不会真实连接 DB
process.env.DB_HOST = process.env.DB_HOST || '127.0.0.1';
process.env.DB_PORT = process.env.DB_PORT || '3306';
process.env.DB_USERNAME = process.env.DB_USERNAME || 'root';
process.env.DB_PASSWORD = process.env.DB_PASSWORD || '';
process.env.DB_DATABASE = process.env.DB_DATABASE || 'wwjcloud_test';
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret_key';
// 避免未处理的Promise拒绝中断测试进程
process.on('unhandledRejection', (err) => {
// eslint-disable-next-line no-console
console.warn('UnhandledRejection in tests:', err);
});

View File

@@ -1,6 +1,6 @@
import { Controller, Post, Body, Get } from '@nestjs/common';
import { TestService } from './test.service';
import { Public } from '../src/common/auth/decorators/public.decorator';
import { Public } from '../src/core/decorators/public.decorator';
@Controller('test')
export class TestController {

View File

@@ -1,12 +1,22 @@
import { Module } from '@nestjs/common';
import { TestController } from './test.controller';
import { TestService } from './test.service';
import { JobsModule } from '../src/common/jobs/jobs.module';
import { EventBusModule } from '../src/common/event-bus/event-bus.module';
import { InMemoryQueueProvider } from './inMemoryQueueProvider';
import { UnifiedQueueService } from '@wwjCore/queue/unifiedQueueService';
import {
TASK_QUEUE_PROVIDER,
EVENT_BUS_PROVIDER,
} from '@wwjCore/interfaces/queue.interface';
@Module({
imports: [JobsModule, EventBusModule],
imports: [],
controllers: [TestController],
providers: [TestService],
providers: [
TestService,
InMemoryQueueProvider,
UnifiedQueueService,
{ provide: TASK_QUEUE_PROVIDER, useExisting: InMemoryQueueProvider },
{ provide: EVENT_BUS_PROVIDER, useExisting: InMemoryQueueProvider },
],
})
export class TestModule {}

View File

@@ -1,22 +1,24 @@
import { Injectable } from '@nestjs/common';
import { JobsService } from '../src/common/jobs/jobs.service';
import { EventBusService } from '../src/common/event-bus/event-bus.service';
import { UnifiedQueueService } from '@wwjCore/queue/unifiedQueueService';
@Injectable()
export class TestService {
constructor(
private readonly jobsService: JobsService,
private readonly eventBusService: EventBusService,
) {}
constructor(private readonly unifiedQueueService: UnifiedQueueService) {}
async publishKafkaEvent(
topic: string,
data: Record<string, any>,
): Promise<void> {
await this.eventBusService.publish(topic, {
event: 'test-event',
data,
await this.unifiedQueueService.publishEvent({
eventType: topic,
aggregateId: 'test-aggregate',
aggregateType: 'Test',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: '0',
idempotencyKey: `key_${Date.now()}`,
traceId: `trace_${Date.now()}`,
data,
});
}
@@ -24,14 +26,14 @@ export class TestService {
type: string,
payload: Record<string, any>,
): Promise<string> {
await this.jobsService.enqueue('test-queue', type, payload, {
await this.unifiedQueueService.addTask('test-queue', type, payload, {
attempts: 3,
backoffMs: 1000,
backoff: { type: 'fixed', delay: 1000 },
removeOnComplete: true,
removeOnFail: false,
});
// 生成一个模拟的 job ID
// 生成一个模拟的 job ID(测试用)
return `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}