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);
});
});