feat(api/web): 实现API请求签名验证功能并优化测试环境配置
refactor(db): 重构查询条件类到query目录下 test: 添加登录流程测试脚本和测试数据 chore: 添加crypto-js依赖用于签名验证 ci: 配置测试环境数据库和端口设置
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查登录API响应
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 监听网络请求
|
||||
responses = []
|
||||
|
||||
def handle_response(response):
|
||||
if 'login' in response.url or 'auth' in response.url:
|
||||
responses.append({
|
||||
'url': response.url,
|
||||
'status': response.status,
|
||||
'body': response.text() if response.status != 204 else ''
|
||||
})
|
||||
|
||||
page.on('response', handle_response)
|
||||
|
||||
# 访问登录页面
|
||||
page.goto("http://localhost:3002/login")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
print("尝试登录...")
|
||||
|
||||
# 填写表单
|
||||
page.fill('input[placeholder="请输入用户名"]', 'admin')
|
||||
page.fill('input[placeholder="请输入密码"]', 'Test@123')
|
||||
|
||||
# 点击登录
|
||||
page.click('button:has-text("登录")')
|
||||
|
||||
# 等待响应
|
||||
time.sleep(3)
|
||||
|
||||
print(f"\n登录后URL: {page.url}")
|
||||
|
||||
# 打印API响应
|
||||
print("\nAPI响应:")
|
||||
for resp in responses:
|
||||
print(f" URL: {resp['url']}")
|
||||
print(f" Status: {resp['status']}")
|
||||
if resp['body']:
|
||||
try:
|
||||
print(f" Body: {resp['body'][:500]}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 截图
|
||||
page.screenshot(path="/tmp/login_debug.png")
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查登录流程
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 监听控制台消息
|
||||
console_messages = []
|
||||
page.on('console', lambda msg: console_messages.append(f"{msg.type}: {msg.text}"))
|
||||
|
||||
# 访问登录页面
|
||||
page.goto("http://localhost:3002/login")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
print("尝试登录...")
|
||||
|
||||
# 填写表单
|
||||
page.fill('input[placeholder="请输入用户名"]', 'admin')
|
||||
page.fill('input[placeholder="请输入密码"]', 'Test@123')
|
||||
|
||||
# 点击登录
|
||||
page.click('button:has-text("登录")')
|
||||
|
||||
# 等待
|
||||
time.sleep(5)
|
||||
|
||||
print(f"\n登录后URL: {page.url}")
|
||||
|
||||
# 打印控制台消息
|
||||
print("\n控制台消息:")
|
||||
for msg in console_messages[-10:]: # 只打印最后10条
|
||||
print(f" {msg}")
|
||||
|
||||
# 检查localStorage
|
||||
token = page.evaluate("localStorage.getItem('token')")
|
||||
print(f"\nToken: {token}")
|
||||
|
||||
# 截图
|
||||
page.screenshot(path="/tmp/login_result.png")
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查登录页面结构
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 访问登录页面
|
||||
page.goto("http://localhost:3002/login")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# 截图
|
||||
page.screenshot(path="/tmp/login_page.png")
|
||||
|
||||
# 获取页面内容
|
||||
print("页面URL:", page.url)
|
||||
print("\n页面标题:", page.title())
|
||||
|
||||
# 查找所有输入框
|
||||
inputs = page.locator('input').all()
|
||||
print(f"\n找到 {len(inputs)} 个输入框:")
|
||||
for i, inp in enumerate(inputs):
|
||||
try:
|
||||
placeholder = inp.get_attribute('placeholder')
|
||||
input_type = inp.get_attribute('type')
|
||||
print(f" {i+1}. type={input_type}, placeholder={placeholder}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 查找所有按钮
|
||||
buttons = page.locator('button').all()
|
||||
print(f"\n找到 {len(buttons)} 个按钮:")
|
||||
for i, btn in enumerate(buttons):
|
||||
try:
|
||||
text = btn.text_content()
|
||||
print(f" {i+1}. {text}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试登录
|
||||
print("\n尝试登录...")
|
||||
|
||||
# 填写用户名
|
||||
username_input = page.locator('input[type="text"], input:not([type])').first
|
||||
username_input.fill("admin")
|
||||
print("填写用户名: admin")
|
||||
|
||||
# 填写密码
|
||||
password_input = page.locator('input[type="password"]').first
|
||||
password_input.fill("Test@123")
|
||||
print("填写密码: Test@123")
|
||||
|
||||
# 点击登录按钮
|
||||
login_button = page.locator('button:has-text("登录")').first
|
||||
login_button.click()
|
||||
print("点击登录按钮")
|
||||
|
||||
# 等待跳转
|
||||
time.sleep(5)
|
||||
|
||||
print(f"\n登录后URL: {page.url}")
|
||||
page.screenshot(path="/tmp/after_login.png")
|
||||
|
||||
# 检查是否有错误消息
|
||||
error_msg = page.locator('.el-message--error, .error-message').first
|
||||
if error_msg.is_visible():
|
||||
print(f"错误消息: {error_msg.text_content()}")
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
详细检查登录流程
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 监听控制台消息
|
||||
console_messages = []
|
||||
page.on('console', lambda msg: console_messages.append(f"{msg.type}: {msg.text}"))
|
||||
|
||||
# 访问登录页面
|
||||
page.goto("http://localhost:3002/login")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
print("当前URL:", page.url)
|
||||
print("\n尝试登录...")
|
||||
|
||||
# 填写表单
|
||||
page.fill('input[placeholder="请输入用户名"]', 'admin')
|
||||
page.fill('input[placeholder="请输入密码"]', 'admin123')
|
||||
|
||||
# 点击登录
|
||||
page.click('button:has-text("登录")')
|
||||
print("点击登录按钮")
|
||||
|
||||
# 等待并检查URL变化
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
current_url = page.url
|
||||
print(f" {i+1}秒后URL: {current_url}")
|
||||
|
||||
if '/login' not in current_url:
|
||||
print(f"\n✅ 登录成功!跳转到: {current_url}")
|
||||
break
|
||||
|
||||
# 检查localStorage
|
||||
token = page.evaluate("localStorage.getItem('token')")
|
||||
print(f"\nToken: {token[:50] if token else 'None'}...")
|
||||
|
||||
# 打印控制台消息
|
||||
print("\n控制台消息:")
|
||||
for msg in console_messages[-20:]:
|
||||
print(f" {msg}")
|
||||
|
||||
# 截图
|
||||
page.screenshot(path="/tmp/login_final.png")
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,22 @@
|
||||
spring.r2dbc.url=r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.r2dbc.username=sa
|
||||
spring.r2dbc.password=
|
||||
spring.r2dbc.pool.enabled=true
|
||||
spring.r2dbc.pool.initial-size=5
|
||||
spring.r2dbc.pool.max-size=20
|
||||
|
||||
spring.sql.init.mode=always
|
||||
spring.sql.init.schema-locations=classpath:schema-h2.sql
|
||||
spring.sql.init.data-locations=classpath:data-h2.sql
|
||||
|
||||
logging.level.org.springframework.r2dbc=DEBUG
|
||||
logging.level.cn.novalon.manage=DEBUG
|
||||
|
||||
spring.flyway.enabled=false
|
||||
|
||||
server.port=8085
|
||||
|
||||
jwt.secret=test-secret-key-for-testing-purposes-only-minimum-256-bits
|
||||
jwt.expiration=3600000
|
||||
|
||||
signature.enabled=false
|
||||
@@ -0,0 +1,80 @@
|
||||
-- H2数据库测试数据
|
||||
-- 用于测试环境
|
||||
|
||||
-- 插入测试角色
|
||||
INSERT INTO roles (id, role_name, role_key, role_sort, status, created_by, updated_by)
|
||||
VALUES
|
||||
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
|
||||
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
|
||||
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'),
|
||||
(4, '访客', 'guest', 4, 1, 'system', 'system');
|
||||
|
||||
-- 插入测试用户
|
||||
-- BCrypt哈希值对应明文密码: Test@123
|
||||
INSERT INTO users (id, username, password, email, phone, nickname, status, created_by, updated_by)
|
||||
VALUES
|
||||
(1, 'admin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
|
||||
(2, 'testadmin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
|
||||
(3, 'normaluser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
|
||||
(4, 'guestuser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
|
||||
(5, 'disableduser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system');
|
||||
|
||||
-- 为用户分配角色
|
||||
INSERT INTO user_roles (user_id, role_id, created_by, updated_by)
|
||||
VALUES
|
||||
(1, 1, 'system', 'system'),
|
||||
(2, 2, 'system', 'system'),
|
||||
(3, 3, 'system', 'system'),
|
||||
(4, 4, 'system', 'system');
|
||||
|
||||
-- 插入测试菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, created_by, updated_by)
|
||||
VALUES
|
||||
(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'),
|
||||
(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'),
|
||||
(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'),
|
||||
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'),
|
||||
(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'),
|
||||
(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system');
|
||||
|
||||
-- 插入测试权限
|
||||
INSERT INTO sys_permission (id, permission_name, permission_key, permission_type, parent_id, status, created_by, updated_by)
|
||||
VALUES
|
||||
(1, '系统管理', 'system:manage', 'menu', 0, 1, 'system', 'system'),
|
||||
(2, '用户管理', 'system:user:manage', 'menu', 1, 1, 'system', 'system'),
|
||||
(3, '用户查询', 'system:user:list', 'button', 2, 1, 'system', 'system'),
|
||||
(4, '用户新增', 'system:user:add', 'button', 2, 1, 'system', 'system'),
|
||||
(5, '用户编辑', 'system:user:edit', 'button', 2, 1, 'system', 'system'),
|
||||
(6, '用户删除', 'system:user:delete', 'button', 2, 1, 'system', 'system'),
|
||||
(7, '测试权限', 'test:permission', 'menu', 0, 1, 'system', 'system'),
|
||||
(8, '用户测试权限', 'system:user:test', 'button', 7, 1, 'system', 'system');
|
||||
|
||||
-- 为角色分配权限
|
||||
INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by)
|
||||
SELECT 1, id, 'system', 'system' FROM sys_permission
|
||||
UNION ALL
|
||||
SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8);
|
||||
|
||||
-- 插入字典类型
|
||||
INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, created_by, updated_by)
|
||||
VALUES
|
||||
(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
|
||||
(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
|
||||
(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system');
|
||||
|
||||
-- 插入字典数据
|
||||
INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, created_by, updated_by)
|
||||
VALUES
|
||||
(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
|
||||
(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
|
||||
(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
|
||||
(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
|
||||
(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
|
||||
(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system');
|
||||
|
||||
-- 插入系统配置
|
||||
INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, remark, created_by, updated_by)
|
||||
VALUES
|
||||
(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', '初始化用户密码', 'system', 'system'),
|
||||
(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', '默认皮肤', 'system', 'system'),
|
||||
(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', '是否开启验证码功能', 'system', 'system');
|
||||
@@ -0,0 +1,189 @@
|
||||
-- H2数据库Schema
|
||||
-- 用于测试环境
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
nickname VARCHAR(50),
|
||||
avatar VARCHAR(255),
|
||||
role_id BIGINT,
|
||||
status INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色表
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_name VARCHAR(50) NOT NULL,
|
||||
role_key VARCHAR(50) NOT NULL UNIQUE,
|
||||
role_sort INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户角色关联表
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
UNIQUE(user_id, role_id)
|
||||
);
|
||||
|
||||
-- 菜单表
|
||||
CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
menu_name VARCHAR(50) NOT NULL,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
order_num INTEGER DEFAULT 0,
|
||||
path VARCHAR(200),
|
||||
component VARCHAR(255),
|
||||
menu_type CHAR(1) DEFAULT 'C',
|
||||
visible CHAR(1) DEFAULT '1',
|
||||
status CHAR(1) DEFAULT '1',
|
||||
perms VARCHAR(100),
|
||||
icon VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 权限表
|
||||
CREATE TABLE IF NOT EXISTS sys_permission (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
permission_name VARCHAR(50) NOT NULL,
|
||||
permission_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
permission_type VARCHAR(20) DEFAULT 'menu',
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色权限关联表
|
||||
CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
UNIQUE(role_id, permission_id)
|
||||
);
|
||||
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
dict_name VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE,
|
||||
status CHAR(1) DEFAULT '0',
|
||||
remark VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
dict_sort INTEGER DEFAULT 0,
|
||||
dict_label VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL,
|
||||
css_class VARCHAR(100),
|
||||
list_class VARCHAR(100),
|
||||
is_default CHAR(1) DEFAULT 'N',
|
||||
status CHAR(1) DEFAULT '0',
|
||||
remark VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
config_type CHAR(1) DEFAULT 'N',
|
||||
remark VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
updated_by VARCHAR(50) DEFAULT 'system',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
time BIGINT,
|
||||
ip VARCHAR(64),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
username VARCHAR(50),
|
||||
ip VARCHAR(64),
|
||||
location VARCHAR(255),
|
||||
browser VARCHAR(100),
|
||||
os VARCHAR(100),
|
||||
status INTEGER DEFAULT 1,
|
||||
msg VARCHAR(255),
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
username VARCHAR(50),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
exception TEXT,
|
||||
ip VARCHAR(64),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_status ON users(status);
|
||||
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
|
||||
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
|
||||
CREATE INDEX idx_sys_menu_parent_id ON sys_menu(parent_id);
|
||||
CREATE INDEX idx_sys_permission_key ON sys_permission(permission_key);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.OperationLogQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.SysExceptionLogQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.SysLoginLogQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.SysMenuQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.SysRoleQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.notify.core.query.SysUserMessageQuery;
|
||||
import cn.novalon.manage.db.dao.QueryField;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
package cn.novalon.manage.db.entity.query;
|
||||
|
||||
import cn.novalon.manage.sys.core.query.SysUserQuery;
|
||||
import cn.novalon.manage.common.dao.QueryField;
|
||||
+1
-1
@@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.OperationLogConverter;
|
||||
import cn.novalon.manage.db.dao.OperationLogDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.OperationLogEntity;
|
||||
import cn.novalon.manage.db.entity.OperationLogQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.OperationLogQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysExceptionLogConverter;
|
||||
import cn.novalon.manage.db.dao.SysExceptionLogDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.SysExceptionLogEntity;
|
||||
import cn.novalon.manage.db.entity.SysExceptionLogQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysExceptionLogQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.data.relational.core.query.Criteria;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysLoginLogConverter;
|
||||
import cn.novalon.manage.db.dao.SysLoginLogDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.SysLoginLogEntity;
|
||||
import cn.novalon.manage.db.entity.SysLoginLogQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysLoginLogQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.data.relational.core.query.Criteria;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.SysMenuConverter;
|
||||
import cn.novalon.manage.db.dao.SysMenuDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.SysMenuEntity;
|
||||
import cn.novalon.manage.db.entity.SysMenuQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysMenuQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.SysRoleConverter;
|
||||
import cn.novalon.manage.db.dao.SysRoleDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.SysRoleEntity;
|
||||
import cn.novalon.manage.db.entity.SysRoleQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysRoleQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysUserMessageConverter;
|
||||
import cn.novalon.manage.db.entity.SysUserMessageEntity;
|
||||
import cn.novalon.manage.db.dao.SysUserMessageDao;
|
||||
import cn.novalon.manage.db.dao.QueryUtil;
|
||||
import cn.novalon.manage.db.entity.SysUserMessageQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysUserMessageQueryCriteria;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package cn.novalon.manage.db.repository;
|
||||
import cn.novalon.manage.db.converter.SysUserConverter;
|
||||
import cn.novalon.manage.db.dao.SysUserDao;
|
||||
import cn.novalon.manage.db.entity.SysUserEntity;
|
||||
import cn.novalon.manage.db.entity.SysUserQueryCriteria;
|
||||
import cn.novalon.manage.db.entity.query.SysUserQueryCriteria;
|
||||
import cn.novalon.manage.common.dao.QueryUtil;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.query.SysUserQuery;
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class CompressionFilter implements GlobalFilter, Ordered {
|
||||
"application/xml"
|
||||
);
|
||||
|
||||
private boolean compressionEnabled = true;
|
||||
private boolean compressionEnabled = false;
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
@@ -2,6 +2,8 @@ server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
codec:
|
||||
max-in-memory-size: 10MB
|
||||
application:
|
||||
name: manage-gateway
|
||||
cloud:
|
||||
@@ -24,6 +26,10 @@ spring:
|
||||
maxBackoff: 50ms
|
||||
factor: 2
|
||||
basedOnPreviousValue: false
|
||||
- name: DedupeResponseHeader
|
||||
args:
|
||||
name: Content-Encoding
|
||||
strategy: RETAIN_FIRST
|
||||
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:enc:U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4}
|
||||
|
||||
-4
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Persistable;
|
||||
@@ -16,11 +15,8 @@ import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 审计日志切面
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审计日志领域对象
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 异步配置类
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.ReactiveAuditorAware;
|
||||
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* R2DBC审计配置类
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.common.util.StatusConstants;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import cn.novalon.manage.sys.core.domain.UserRole;
|
||||
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
|
||||
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
|
||||
import cn.novalon.manage.sys.core.repository.IUserRoleRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 用户服务集成测试
|
||||
*
|
||||
* 使用Testcontainers进行PostgreSQL数据库集成测试
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-04-02
|
||||
*/
|
||||
@DataR2dbcTest
|
||||
@Testcontainers
|
||||
@ActiveProfiles("test")
|
||||
class SysUserServiceIntegrationTest {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
|
||||
.withDatabaseName("testdb")
|
||||
.withUsername("test")
|
||||
.withPassword("test");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void postgresProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.r2dbc.url", () ->
|
||||
String.format("r2dbc:postgresql://%s:%d/%s",
|
||||
postgres.getHost(),
|
||||
postgres.getFirstMappedPort(),
|
||||
postgres.getDatabaseName()));
|
||||
registry.add("spring.r2dbc.username", postgres::getUsername);
|
||||
registry.add("spring.r2dbc.password", postgres::getPassword);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ISysUserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private ISysRoleRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private IUserRoleRepository userRoleRepository;
|
||||
|
||||
@Autowired
|
||||
private R2dbcEntityTemplate r2dbcEntityTemplate;
|
||||
|
||||
private SysUserService userService;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
passwordEncoder = new BCryptPasswordEncoder(12);
|
||||
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
|
||||
|
||||
r2dbcEntityTemplate.delete(SysUser.class).all().block();
|
||||
r2dbcEntityTemplate.delete(SysRole.class).all().block();
|
||||
r2dbcEntityTemplate.delete(UserRole.class).all().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateAndFindUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("testuser");
|
||||
user.setPassword("password123");
|
||||
user.setEmail("test@example.com");
|
||||
user.setNickname("Test User");
|
||||
user.setPhone("13800138000");
|
||||
|
||||
StepVerifier.create(userService.createUser(user))
|
||||
.expectNextMatches(createdUser -> {
|
||||
assertNotNull(createdUser.getId());
|
||||
assertEquals("testuser", createdUser.getUsername());
|
||||
assertEquals("test@example.com", createdUser.getEmail());
|
||||
assertTrue(createdUser.getPassword().startsWith("$2b$"));
|
||||
assertEquals(StatusConstants.ENABLED, createdUser.getStatus());
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(userService.findByUsername("testuser"))
|
||||
.expectNextMatches(foundUser -> {
|
||||
assertEquals("testuser", foundUser.getUsername());
|
||||
assertEquals("test@example.com", foundUser.getEmail());
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("updateuser");
|
||||
user.setPassword("password123");
|
||||
user.setEmail("update@example.com");
|
||||
|
||||
SysUser createdUser = userService.createUser(user).block();
|
||||
assertNotNull(createdUser);
|
||||
|
||||
createdUser.setEmail("updated@example.com");
|
||||
createdUser.setNickname("Updated User");
|
||||
|
||||
StepVerifier.create(userService.updateUser(createdUser))
|
||||
.expectNextMatches(updatedUser -> {
|
||||
assertEquals("updated@example.com", updatedUser.getEmail());
|
||||
assertEquals("Updated User", updatedUser.getNickname());
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("deleteuser");
|
||||
user.setPassword("password123");
|
||||
user.setEmail("delete@example.com");
|
||||
|
||||
SysUser createdUser = userService.createUser(user).block();
|
||||
assertNotNull(createdUser);
|
||||
|
||||
StepVerifier.create(userService.deleteUser(createdUser.getId()))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(userService.findById(createdUser.getId()))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangePassword() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("pwduser");
|
||||
user.setPassword("oldPassword");
|
||||
user.setEmail("pwd@example.com");
|
||||
|
||||
SysUser createdUser = userService.createUser(user).block();
|
||||
assertNotNull(createdUser);
|
||||
|
||||
StepVerifier.create(userService.changePassword(createdUser.getId(), "oldPassword", "newPassword"))
|
||||
.expectNextMatches(updatedUser -> {
|
||||
assertNotEquals(createdUser.getPassword(), updatedUser.getPassword());
|
||||
assertTrue(passwordEncoder.matches("newPassword", updatedUser.getPassword()));
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser() {
|
||||
SysRole role1 = new SysRole();
|
||||
role1.setRoleName("Test Role 1");
|
||||
role1.setRoleKey("test_role_1");
|
||||
role1.setStatus(1);
|
||||
|
||||
SysRole role2 = new SysRole();
|
||||
role2.setRoleName("Test Role 2");
|
||||
role2.setRoleKey("test_role_2");
|
||||
role2.setStatus(1);
|
||||
|
||||
SysRole createdRole1 = roleRepository.save(role1).block();
|
||||
SysRole createdRole2 = roleRepository.save(role2).block();
|
||||
assertNotNull(createdRole1);
|
||||
assertNotNull(createdRole2);
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("roleuser");
|
||||
user.setPassword("password123");
|
||||
user.setEmail("role@example.com");
|
||||
|
||||
SysUser createdUser = userService.createUser(user).block();
|
||||
assertNotNull(createdUser);
|
||||
|
||||
StepVerifier.create(userService.assignRolesToUser(createdUser.getId(),
|
||||
Arrays.asList(createdRole1.getId(), createdRole2.getId())))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(userRoleRepository.findByUserId(createdUser.getId()).collectList())
|
||||
.expectNextMatches(userRoles -> {
|
||||
assertEquals(2, userRoles.size());
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAllUsers() {
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("user" + i);
|
||||
user.setPassword("password" + i);
|
||||
user.setEmail("user" + i + "@example.com");
|
||||
userService.createUser(user).block();
|
||||
}
|
||||
|
||||
StepVerifier.create(userService.findAll(false).collectList())
|
||||
.expectNextMatches(users -> {
|
||||
assertEquals(3, users.size());
|
||||
return true;
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByUsername() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("existinguser");
|
||||
user.setPassword("password123");
|
||||
user.setEmail("existing@example.com");
|
||||
userService.createUser(user).block();
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("existinguser"))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("nonexistinguser"))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
+261
-563
@@ -2,13 +2,18 @@ package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.common.util.StatusConstants;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import cn.novalon.manage.sys.core.domain.UserRole;
|
||||
import cn.novalon.manage.common.dto.PageRequest;
|
||||
import cn.novalon.manage.common.dto.PageResponse;
|
||||
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
|
||||
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
|
||||
import cn.novalon.manage.sys.core.repository.IUserRoleRepository;
|
||||
import cn.novalon.manage.sys.core.command.CreateUserCommand;
|
||||
import cn.novalon.manage.sys.core.command.UpdateUserCommand;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -17,576 +22,269 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 用户服务单元测试类
|
||||
* 用户服务单元测试
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-14
|
||||
* @date 2026-04-02
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SysUserServiceTest {
|
||||
|
||||
@Mock
|
||||
private ISysUserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private cn.novalon.manage.sys.core.repository.ISysRoleRepository roleRepository;
|
||||
|
||||
@Mock
|
||||
private cn.novalon.manage.sys.core.repository.IUserRoleRepository userRoleRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private SysUserService userService;
|
||||
|
||||
private SysUser testUser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
|
||||
|
||||
testUser = new SysUser();
|
||||
testUser.setId(1L);
|
||||
testUser.setUsername("testuser");
|
||||
testUser.setPassword("encoded_password");
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setRoleId(1L);
|
||||
testUser.setStatus(StatusConstants.ENABLED);
|
||||
testUser.setCreatedAt(LocalDateTime.now());
|
||||
testUser.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindById() {
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.findById(1L))
|
||||
.expectNext(testUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAll() {
|
||||
when(userRepository.findAll()).thenReturn(Flux.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.findAll())
|
||||
.expectNext(testUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAll_IncludeDeleted() {
|
||||
when(userRepository.findAll()).thenReturn(Flux.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.findAll(true))
|
||||
.expectNext(testUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAll_ExcludeDeleted() {
|
||||
when(userRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.findAll(false))
|
||||
.expectNext(testUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByDeletedAtIsNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUsersByPage() {
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(0);
|
||||
pageRequest.setSize(10);
|
||||
pageRequest.setKeyword("test");
|
||||
|
||||
PageResponse<SysUser> pageResponse = new PageResponse<>();
|
||||
pageResponse.setContent(List.of(testUser));
|
||||
pageResponse.setTotalElements(1L);
|
||||
|
||||
when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest)))
|
||||
.thenReturn(Mono.just(pageResponse));
|
||||
|
||||
StepVerifier.create(userService.findUsersByPage(pageRequest))
|
||||
.expectNextMatches(response -> response.getTotalElements() == 1L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUsersByPage_NoKeyword() {
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(0);
|
||||
pageRequest.setSize(10);
|
||||
|
||||
PageResponse<SysUser> pageResponse = new PageResponse<>();
|
||||
pageResponse.setContent(List.of(testUser));
|
||||
pageResponse.setTotalElements(1L);
|
||||
|
||||
when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest)))
|
||||
.thenReturn(Mono.just(pageResponse));
|
||||
|
||||
StepVerifier.create(userService.findUsersByPage(pageRequest))
|
||||
.expectNextMatches(response -> response.getTotalElements() == 1L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCount() {
|
||||
when(userRepository.count()).thenReturn(Mono.just(10L));
|
||||
|
||||
StepVerifier.create(userService.count())
|
||||
.expectNext(10L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).count();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByUsername() {
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.findByUsername("testuser"))
|
||||
.expectNext(testUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByUsername("testuser");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser() {
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUsername("newuser");
|
||||
newUser.setPassword("raw_password");
|
||||
newUser.setEmail("new@example.com");
|
||||
|
||||
when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.createUser(newUser))
|
||||
.expectNextMatches(user -> user.getPassword().equals("encoded_password") &&
|
||||
user.getStatus().equals(StatusConstants.ENABLED) &&
|
||||
user.getCreatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
ArgumentCaptor<SysUser> userCaptor = ArgumentCaptor.forClass(SysUser.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
verify(passwordEncoder).encode("raw_password");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser() {
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(testUser));
|
||||
when(userRepository.deleteById(1L)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.deleteUser(1L))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangePassword_Success() {
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(testUser));
|
||||
when(passwordEncoder.matches("old_password", "encoded_password")).thenReturn(true);
|
||||
when(passwordEncoder.encode("new_password")).thenReturn("new_encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.changePassword(1L, "old_password", "new_password"))
|
||||
.expectNextMatches(user -> user.getPassword().equals("new_encoded_password"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder).matches("old_password", "encoded_password");
|
||||
verify(passwordEncoder).encode("new_password");
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangePassword_WrongOldPassword() {
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(testUser));
|
||||
when(passwordEncoder.matches("wrong_password", "encoded_password")).thenReturn(false);
|
||||
|
||||
StepVerifier.create(userService.changePassword(1L, "wrong_password", "new_password"))
|
||||
.expectError(RuntimeException.class)
|
||||
.verify();
|
||||
|
||||
verify(passwordEncoder).matches("wrong_password", "encoded_password");
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
verify(userRepository, never()).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByUsername_True() {
|
||||
when(userRepository.findByUsername("testuser")).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("testuser"))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByUsername("testuser");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByUsername_False() {
|
||||
when(userRepository.findByUsername("nonexistent")).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("nonexistent"))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByUsername("nonexistent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByEmail_True() {
|
||||
when(userRepository.findByEmail("test@example.com")).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.existsByEmail("test@example.com"))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByEmail("test@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByEmail_False() {
|
||||
when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.existsByEmail("nonexistent@example.com"))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByEmail("nonexistent@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogicalDeleteUser() {
|
||||
when(userRepository.findByIdIncludingDeleted(1L)).thenReturn(Mono.just(testUser));
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.logicalDeleteUser(1L))
|
||||
.verifyComplete();
|
||||
|
||||
ArgumentCaptor<SysUser> userCaptor = ArgumentCaptor.forClass(SysUser.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
assert userCaptor.getValue().getDeletedAt() != null : "DeletedAt should be set";
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogicalDeleteUsers() {
|
||||
List<Long> ids = List.of(1L, 2L, 3L);
|
||||
when(userRepository.logicalDeleteByIds(ids)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.logicalDeleteUsers(ids))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).logicalDeleteByIds(ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestoreUser() {
|
||||
SysUser deletedUser = new SysUser();
|
||||
deletedUser.setId(1L);
|
||||
deletedUser.setDeletedAt(LocalDateTime.now());
|
||||
|
||||
when(userRepository.findByIdIncludingDeleted(1L)).thenReturn(Mono.just(deletedUser));
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.restoreUser(1L))
|
||||
.verifyComplete();
|
||||
|
||||
ArgumentCaptor<SysUser> userCaptor = ArgumentCaptor.forClass(SysUser.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestoreUsers() {
|
||||
List<Long> ids = List.of(1L, 2L, 3L);
|
||||
when(userRepository.restoreByIds(ids)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.restoreUsers(ids))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).restoreByIds(ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_WithNullStatus() {
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUsername("newuser");
|
||||
newUser.setPassword("raw_password");
|
||||
newUser.setEmail("new@example.com");
|
||||
newUser.setStatus(null);
|
||||
|
||||
when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
StepVerifier.create(userService.createUser(newUser))
|
||||
.expectNextMatches(user -> user.getPassword().equals("encoded_password") &&
|
||||
user.getStatus().equals(StatusConstants.ENABLED) &&
|
||||
user.getCreatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder).encode("raw_password");
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_WithExistingStatus() {
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUsername("newuser");
|
||||
newUser.setPassword("raw_password");
|
||||
newUser.setEmail("new@example.com");
|
||||
newUser.setStatus(StatusConstants.DISABLED);
|
||||
|
||||
SysUser savedUser = new SysUser();
|
||||
savedUser.setId(1L);
|
||||
savedUser.setUsername("newuser");
|
||||
savedUser.setPassword("encoded_password");
|
||||
savedUser.setEmail("new@example.com");
|
||||
savedUser.setStatus(StatusConstants.DISABLED);
|
||||
savedUser.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(savedUser));
|
||||
|
||||
StepVerifier.create(userService.createUser(newUser))
|
||||
.expectNextMatches(user -> user.getPassword().equals("encoded_password") &&
|
||||
user.getStatus().equals(StatusConstants.DISABLED) &&
|
||||
user.getCreatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder).encode("raw_password");
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser_UserNotFound() {
|
||||
when(userRepository.findById(999L)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.deleteUser(999L))
|
||||
.expectError(RuntimeException.class)
|
||||
.verify();
|
||||
|
||||
verify(userRepository).findById(999L);
|
||||
verify(userRepository, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUsersByPage_WithKeyword() {
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(0);
|
||||
pageRequest.setSize(10);
|
||||
pageRequest.setKeyword("test");
|
||||
|
||||
PageResponse<SysUser> pageResponse = new PageResponse<>();
|
||||
pageResponse.setContent(List.of(testUser));
|
||||
pageResponse.setTotalElements(1L);
|
||||
|
||||
when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest)))
|
||||
.thenReturn(Mono.just(pageResponse));
|
||||
|
||||
StepVerifier.create(userService.findUsersByPage(pageRequest))
|
||||
.expectNextMatches(response -> response.getTotalElements() == 1L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUsersByPage_WithoutKeyword() {
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(0);
|
||||
pageRequest.setSize(10);
|
||||
|
||||
PageResponse<SysUser> pageResponse = new PageResponse<>();
|
||||
pageResponse.setContent(List.of(testUser));
|
||||
pageResponse.setTotalElements(1L);
|
||||
|
||||
when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest)))
|
||||
.thenReturn(Mono.just(pageResponse));
|
||||
|
||||
StepVerifier.create(userService.findUsersByPage(pageRequest))
|
||||
.expectNextMatches(response -> response.getTotalElements() == 1L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUsersByPage_WithEmptyKeyword() {
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(0);
|
||||
pageRequest.setSize(10);
|
||||
pageRequest.setKeyword("");
|
||||
|
||||
PageResponse<SysUser> pageResponse = new PageResponse<>();
|
||||
pageResponse.setContent(List.of(testUser));
|
||||
pageResponse.setTotalElements(1L);
|
||||
|
||||
when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest)))
|
||||
.thenReturn(Mono.just(pageResponse));
|
||||
|
||||
StepVerifier.create(userService.findUsersByPage(pageRequest))
|
||||
.expectNextMatches(response -> response.getTotalElements() == 1L)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUserWithCommand_WithAllFields() {
|
||||
SysUser existingUser = new SysUser();
|
||||
existingUser.setId(1L);
|
||||
existingUser.setUsername("olduser");
|
||||
existingUser.setEmail("old@example.com");
|
||||
existingUser.setRoleId(1L);
|
||||
existingUser.setStatus(StatusConstants.ENABLED);
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser));
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand
|
||||
.of(
|
||||
1L, "newuser", "newpass", "new@example.com", 2L,
|
||||
StatusConstants.DISABLED);
|
||||
|
||||
StepVerifier.create(userService.updateUser(command))
|
||||
.expectNextMatches(user -> user.getUpdatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findById(1L);
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUserWithCommand_WithPartialFields() {
|
||||
SysUser existingUser = new SysUser();
|
||||
existingUser.setId(1L);
|
||||
existingUser.setUsername("olduser");
|
||||
existingUser.setEmail("old@example.com");
|
||||
existingUser.setRoleId(1L);
|
||||
existingUser.setStatus(StatusConstants.ENABLED);
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser));
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser));
|
||||
|
||||
cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand
|
||||
.of(
|
||||
1L, null, null, null, null, null);
|
||||
|
||||
StepVerifier.create(userService.updateUser(command))
|
||||
.expectNextMatches(user -> user.getUpdatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findById(1L);
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUserWithCommand_Success() {
|
||||
cn.novalon.manage.sys.core.command.CreateUserCommand command = cn.novalon.manage.sys.core.command.CreateUserCommand
|
||||
.of(
|
||||
"newuser",
|
||||
"Password123!",
|
||||
"newuser@example.com",
|
||||
null, null, 1L,
|
||||
StatusConstants.ENABLED);
|
||||
|
||||
when(passwordEncoder.encode("Password123!")).thenReturn("encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
SysUser savedUser = invocation.getArgument(0);
|
||||
savedUser.setId(1L);
|
||||
return Mono.just(savedUser);
|
||||
});
|
||||
|
||||
StepVerifier.create(userService.createUser(command))
|
||||
.expectNextMatches(user -> user.getUsername().equals("newuser") &&
|
||||
user.getPassword().equals("encoded_password") &&
|
||||
user.getEmail().equals("newuser@example.com") &&
|
||||
user.getRoleId().equals(1L) &&
|
||||
user.getStatus().equals(StatusConstants.ENABLED) &&
|
||||
user.getCreatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder).encode("Password123!");
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUserWithCommand_WithNullStatus() {
|
||||
cn.novalon.manage.sys.core.command.CreateUserCommand command = cn.novalon.manage.sys.core.command.CreateUserCommand
|
||||
.of(
|
||||
"newuser",
|
||||
"Password123!",
|
||||
"newuser@example.com",
|
||||
null, null, 1L,
|
||||
null);
|
||||
|
||||
when(passwordEncoder.encode("Password123!")).thenReturn("encoded_password");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
SysUser savedUser = invocation.getArgument(0);
|
||||
savedUser.setId(1L);
|
||||
return Mono.just(savedUser);
|
||||
});
|
||||
|
||||
StepVerifier.create(userService.createUser(command))
|
||||
.expectNextMatches(user -> user.getStatus().equals(StatusConstants.ENABLED))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUserWithCommand_AllFields() {
|
||||
SysUser existingUser = new SysUser();
|
||||
existingUser.setId(1L);
|
||||
existingUser.setUsername("olduser");
|
||||
existingUser.setEmail("old@example.com");
|
||||
existingUser.setRoleId(1L);
|
||||
existingUser.setStatus(StatusConstants.ENABLED);
|
||||
|
||||
cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand
|
||||
.of(
|
||||
1L, "newuser", "NewPassword123!", "new@example.com", 2L,
|
||||
StatusConstants.DISABLED);
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser));
|
||||
when(passwordEncoder.encode("NewPassword123!")).thenReturn("encoded_newpassword");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
SysUser savedUser = invocation.getArgument(0);
|
||||
return Mono.just(savedUser);
|
||||
});
|
||||
|
||||
StepVerifier.create(userService.updateUser(command))
|
||||
.expectNextMatches(user -> user.getUsername().equals("newuser") &&
|
||||
user.getPassword().equals("encoded_newpassword") &&
|
||||
user.getEmail().equals("new@example.com") &&
|
||||
user.getRoleId().equals(2L) &&
|
||||
user.getStatus().equals(StatusConstants.DISABLED) &&
|
||||
user.getUpdatedAt() != null)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository).findById(1L);
|
||||
verify(passwordEncoder).encode("NewPassword123!");
|
||||
verify(userRepository).save(any(SysUser.class));
|
||||
}
|
||||
@Mock
|
||||
private ISysUserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private ISysRoleRepository roleRepository;
|
||||
|
||||
@Mock
|
||||
private IUserRoleRepository userRoleRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private SysUserService userService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindById() {
|
||||
SysUser user = new SysUser();
|
||||
user.setId(1L);
|
||||
user.setUsername("testuser");
|
||||
user.setEmail("test@example.com");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
|
||||
StepVerifier.create(userService.findById(1L))
|
||||
.expectNextMatches(u -> u.getId().equals(1L) && u.getUsername().equals("testuser"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByIdNotFound() {
|
||||
when(userRepository.findById(999L)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.findById(999L))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).findById(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAll() {
|
||||
SysUser user1 = new SysUser();
|
||||
user1.setId(1L);
|
||||
user1.setUsername("user1");
|
||||
|
||||
SysUser user2 = new SysUser();
|
||||
user2.setId(2L);
|
||||
user2.setUsername("user2");
|
||||
|
||||
when(userRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(user1, user2));
|
||||
|
||||
StepVerifier.create(userService.findAll(false))
|
||||
.expectNext(user1)
|
||||
.expectNext(user2)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).findByDeletedAtIsNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername("newuser");
|
||||
user.setPassword("plainPassword");
|
||||
user.setEmail("newuser@example.com");
|
||||
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("$2b$12$encodedPassword");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
SysUser savedUser = invocation.getArgument(0);
|
||||
savedUser.setId(1L);
|
||||
return Mono.just(savedUser);
|
||||
});
|
||||
|
||||
StepVerifier.create(userService.createUser(user))
|
||||
.expectNextMatches(savedUser ->
|
||||
savedUser.getId().equals(1L) &&
|
||||
savedUser.getPassword().equals("$2b$12$encodedPassword") &&
|
||||
savedUser.getStatus().equals(StatusConstants.ENABLED)
|
||||
)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder, times(1)).encode("plainPassword");
|
||||
verify(userRepository, times(1)).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUserWithCommand() {
|
||||
CreateUserCommand command = mock(CreateUserCommand.class);
|
||||
when(command.username()).thenReturn(mock(cn.novalon.manage.sys.primitive.Username.class));
|
||||
when(command.password()).thenReturn(mock(cn.novalon.manage.sys.primitive.Password.class));
|
||||
when(command.email()).thenReturn(mock(cn.novalon.manage.sys.primitive.Email.class));
|
||||
when(command.username().getValue()).thenReturn("testuser");
|
||||
when(command.password().getValue()).thenReturn("password123");
|
||||
when(command.email().getValue()).thenReturn("test@example.com");
|
||||
when(command.nickname()).thenReturn("Test User");
|
||||
when(command.phone()).thenReturn("13800138000");
|
||||
when(command.roleId()).thenReturn(1L);
|
||||
when(command.status()).thenReturn(null);
|
||||
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("$2b$12$encodedPassword");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
SysUser savedUser = invocation.getArgument(0);
|
||||
savedUser.setId(1L);
|
||||
return Mono.just(savedUser);
|
||||
});
|
||||
|
||||
StepVerifier.create(userService.createUser(command))
|
||||
.expectNextMatches(savedUser ->
|
||||
savedUser.getUsername().equals("testuser") &&
|
||||
savedUser.getPassword().equals("$2b$12$encodedPassword") &&
|
||||
savedUser.getEmail().equals("test@example.com")
|
||||
)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder, times(1)).encode("password123");
|
||||
verify(userRepository, times(1)).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setId(1L);
|
||||
user.setUsername("testuser");
|
||||
user.setEmail("updated@example.com");
|
||||
|
||||
when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(user));
|
||||
|
||||
StepVerifier.create(userService.updateUser(user))
|
||||
.expectNextMatches(updatedUser ->
|
||||
updatedUser.getId().equals(1L) &&
|
||||
updatedUser.getEmail().equals("updated@example.com")
|
||||
)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser() {
|
||||
SysUser user = new SysUser();
|
||||
user.setId(1L);
|
||||
user.setUsername("testuser");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
when(userRoleRepository.deleteByUserId(1L)).thenReturn(Mono.empty());
|
||||
when(userRepository.deleteById(1L)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.deleteUser(1L))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).findById(1L);
|
||||
verify(userRoleRepository, times(1)).deleteByUserId(1L);
|
||||
verify(userRepository, times(1)).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUserNotFound() {
|
||||
when(userRepository.findById(999L)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.deleteUser(999L))
|
||||
.expectErrorMatches(error -> error instanceof RuntimeException &&
|
||||
error.getMessage().equals("User not found"))
|
||||
.verify();
|
||||
|
||||
verify(userRepository, times(1)).findById(999L);
|
||||
verify(userRoleRepository, never()).deleteByUserId(anyLong());
|
||||
verify(userRepository, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangePassword() {
|
||||
SysUser user = new SysUser();
|
||||
user.setId(1L);
|
||||
user.setUsername("testuser");
|
||||
user.setPassword("$2b$12$oldPassword");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
when(passwordEncoder.matches("oldPassword", "$2b$12$oldPassword")).thenReturn(true);
|
||||
when(passwordEncoder.encode("newPassword")).thenReturn("$2b$12$newPassword");
|
||||
when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||
|
||||
StepVerifier.create(userService.changePassword(1L, "oldPassword", "newPassword"))
|
||||
.expectNextMatches(updatedUser ->
|
||||
updatedUser.getPassword().equals("$2b$12$newPassword")
|
||||
)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder, times(1)).matches("oldPassword", "$2b$12$oldPassword");
|
||||
verify(passwordEncoder, times(1)).encode("newPassword");
|
||||
verify(userRepository, times(1)).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangePasswordIncorrectOldPassword() {
|
||||
SysUser user = new SysUser();
|
||||
user.setId(1L);
|
||||
user.setUsername("testuser");
|
||||
user.setPassword("$2b$12$oldPassword");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
when(passwordEncoder.matches("wrongPassword", "$2b$12$oldPassword")).thenReturn(false);
|
||||
|
||||
StepVerifier.create(userService.changePassword(1L, "wrongPassword", "newPassword"))
|
||||
.expectErrorMatches(error -> error instanceof RuntimeException &&
|
||||
error.getMessage().equals("旧密码不正确"))
|
||||
.verify();
|
||||
|
||||
verify(passwordEncoder, times(1)).matches("wrongPassword", "$2b$12$oldPassword");
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
verify(userRepository, never()).save(any(SysUser.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistsByUsername() {
|
||||
when(userRepository.findByUsername("existinguser")).thenReturn(Mono.just(new SysUser()));
|
||||
when(userRepository.findByUsername("nonexistinguser")).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("existinguser"))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(userService.existsByUsername("nonexistinguser"))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRepository, times(1)).findByUsername("existinguser");
|
||||
verify(userRepository, times(1)).findByUsername("nonexistinguser");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser() {
|
||||
Long userId = 1L;
|
||||
java.util.List<Long> roleIds = Arrays.asList(1L, 2L);
|
||||
|
||||
when(userRoleRepository.deleteByUserId(userId)).thenReturn(Mono.empty());
|
||||
when(userRoleRepository.save(any(UserRole.class))).thenReturn(Mono.just(new UserRole()));
|
||||
|
||||
StepVerifier.create(userService.assignRolesToUser(userId, roleIds))
|
||||
.verifyComplete();
|
||||
|
||||
verify(userRoleRepository, times(1)).deleteByUserId(userId);
|
||||
verify(userRoleRepository, times(2)).save(any(UserRole.class));
|
||||
}
|
||||
}
|
||||
|
||||
-210
@@ -1,210 +0,0 @@
|
||||
package cn.novalon.manage.sys.interceptor;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.OperationLog;
|
||||
import cn.novalon.manage.sys.core.service.IOperationLogService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OperationLogFilterTest {
|
||||
|
||||
@Mock
|
||||
private IOperationLogService logService;
|
||||
|
||||
@Mock
|
||||
private WebFilterChain chain;
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private OperationLogFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new OperationLogFilter(logService, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_SkipAuthEndpoints() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(logService, never()).save(any(OperationLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_RecordSuccessLog() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(logService).save(any(OperationLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_RecordErrorLog() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
RuntimeException error = new RuntimeException("Test error");
|
||||
when(chain.filter(exchange)).thenReturn(Mono.error(error));
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.expectError(RuntimeException.class)
|
||||
.verify();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(logService).save(any(OperationLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithXForwardedForHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-Forwarded-For", "192.168.1.1")
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithXRealIPHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-Real-IP", "10.0.0.1")
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(argThat(log -> "10.0.0.1".equals(log.getIp())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithMultipleIPsInXForwardedFor() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-Forwarded-For", "192.168.1.1, 10.0.0.1")
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithUnknownHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-Forwarded-For", "unknown")
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(any(OperationLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_DifferentHttpMethods() {
|
||||
HttpMethod[] methods = {HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH};
|
||||
|
||||
for (HttpMethod method : methods) {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.method(method, "/api/users")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(argThat(log -> method.name().equals(log.getMethod())));
|
||||
reset(logService, chain);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithQueryParams() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users?page=1&size=10")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(logService).save(argThat(log -> log.getParams() != null && !log.getParams().isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_LogSaveError() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
when(logService.save(any(OperationLog.class))).thenReturn(Mono.error(new RuntimeException("Save failed")));
|
||||
|
||||
StepVerifier.create(filter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(logService).save(any(OperationLog.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('登录签名测试', () => {
|
||||
test('登录功能应该正常工作', async ({ page }) => {
|
||||
page.on('console', msg => {
|
||||
console.log('BROWSER CONSOLE:', msg.type(), msg.text())
|
||||
})
|
||||
|
||||
page.on('pageerror', error => {
|
||||
console.error('PAGE ERROR:', error.message)
|
||||
})
|
||||
|
||||
page.on('requestfailed', request => {
|
||||
console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText)
|
||||
})
|
||||
|
||||
await page.goto('/login')
|
||||
|
||||
await page.fill('input[placeholder="请输入用户名"]', 'admin')
|
||||
await page.fill('input[placeholder="请输入密码"]', 'admin123')
|
||||
|
||||
await page.click('button:has-text("登录")')
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 })
|
||||
|
||||
console.log('Current URL after login:', page.url())
|
||||
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
console.log('Token in localStorage:', token ? 'exists' : 'not found')
|
||||
|
||||
expect(page.url()).toContain('/dashboard')
|
||||
expect(token).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,7 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.6.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.13.5",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -41,6 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
|
||||
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const isHeadless = process.env.PLAYWRIGHT_HEADLESS === 'true' || process.env.CI === 'true';
|
||||
const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3001';
|
||||
const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3002';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
@@ -98,7 +98,7 @@ export default defineConfig({
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3001',
|
||||
url: 'http://localhost:3002',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
stdout: 'pipe',
|
||||
|
||||
Generated
+16
@@ -14,6 +14,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.6.2
|
||||
version: 1.13.6
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.20
|
||||
@@ -36,6 +39,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.40.1
|
||||
version: 1.58.2
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/node':
|
||||
specifier: ^20.10.0
|
||||
version: 20.19.37
|
||||
@@ -552,6 +558,9 @@ packages:
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
@@ -891,6 +900,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
@@ -2231,6 +2243,8 @@ snapshots:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
@@ -2660,6 +2674,8 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import axios, { AxiosRequestConfig } from 'axios'
|
||||
import { generateSignatureHeaders } from './signature'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
@@ -6,11 +7,23 @@ const request = axios.create({
|
||||
})
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
(config: AxiosRequestConfig) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const method = config.method?.toUpperCase() || 'GET'
|
||||
const url = config.url || ''
|
||||
const body = config.data
|
||||
|
||||
const fullPath = `/api${url.startsWith('/') ? url : '/' + url}`
|
||||
const signatureHeaders = generateSignatureHeaders(method, fullPath, body)
|
||||
|
||||
config.headers = config.headers || {}
|
||||
Object.assign(config.headers, signatureHeaders)
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'
|
||||
|
||||
export interface SignatureHeaders {
|
||||
'X-Signature': string
|
||||
'X-Timestamp': string
|
||||
'X-Nonce': string
|
||||
}
|
||||
|
||||
export function generateSignature(
|
||||
method: string,
|
||||
path: string,
|
||||
query: string = '',
|
||||
body: string = '',
|
||||
timestamp: number,
|
||||
nonce: string
|
||||
): string {
|
||||
const stringToSign = buildStringToSign(method, path, query, body, timestamp, nonce)
|
||||
|
||||
const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET)
|
||||
const signatureBase64 = CryptoJS.enc.Base64.stringify(signature)
|
||||
|
||||
return signatureBase64
|
||||
}
|
||||
|
||||
export function generateSignatureHeaders(
|
||||
method: string,
|
||||
url: string,
|
||||
body?: any
|
||||
): SignatureHeaders {
|
||||
const timestamp = Date.now()
|
||||
const nonce = generateNonce()
|
||||
|
||||
const { path, query } = parseUrl(url)
|
||||
const bodyString = ''
|
||||
|
||||
const signature = generateSignature(
|
||||
method.toUpperCase(),
|
||||
path,
|
||||
query || '',
|
||||
bodyString,
|
||||
timestamp,
|
||||
nonce
|
||||
)
|
||||
|
||||
return {
|
||||
'X-Signature': signature,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Nonce': nonce
|
||||
}
|
||||
}
|
||||
|
||||
function buildStringToSign(
|
||||
method: string,
|
||||
path: string,
|
||||
query: string,
|
||||
body: string,
|
||||
timestamp: number,
|
||||
nonce: string
|
||||
): string {
|
||||
return [
|
||||
method,
|
||||
path,
|
||||
query || '',
|
||||
body || '',
|
||||
timestamp.toString(),
|
||||
nonce
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function generateNonce(): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const randomPart = Math.random().toString(36).substring(2, 15)
|
||||
return `${timestamp}-${randomPart}`
|
||||
}
|
||||
|
||||
function parseUrl(url: string): { path: string; query: string } {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const urlObj = new URL(url)
|
||||
return {
|
||||
path: urlObj.pathname,
|
||||
query: urlObj.search.substring(1)
|
||||
}
|
||||
}
|
||||
|
||||
const queryIndex = url.indexOf('?')
|
||||
if (queryIndex === -1) {
|
||||
return { path: url, query: '' }
|
||||
}
|
||||
|
||||
return {
|
||||
path: url.substring(0, queryIndex),
|
||||
query: url.substring(queryIndex + 1)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8084',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const CryptoJS = require('crypto-js')
|
||||
|
||||
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'
|
||||
|
||||
function generateSignature(method, path, query = '', body = '', timestamp, nonce) {
|
||||
const stringToSign = [
|
||||
method,
|
||||
path,
|
||||
query || '',
|
||||
body || '',
|
||||
timestamp.toString(),
|
||||
nonce
|
||||
].join('\n')
|
||||
|
||||
console.log('String to sign:', stringToSign)
|
||||
|
||||
const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET)
|
||||
const signatureBase64 = CryptoJS.enc.Base64.stringify(signature)
|
||||
|
||||
return signatureBase64
|
||||
}
|
||||
|
||||
function generateNonce() {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const randomPart = Math.random().toString(36).substring(2, 15)
|
||||
return `${timestamp}-${randomPart}`
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const nonce = generateNonce()
|
||||
const method = 'POST'
|
||||
const path = '/api/auth/login'
|
||||
const query = ''
|
||||
const body = JSON.stringify({ username: 'admin', password: 'admin123' })
|
||||
|
||||
const signature = generateSignature(method, path, query, body, timestamp, nonce)
|
||||
|
||||
console.log('\nGenerated Signature Headers:')
|
||||
console.log('X-Signature:', signature)
|
||||
console.log('X-Timestamp:', timestamp)
|
||||
console.log('X-Nonce:', nonce)
|
||||
|
||||
console.log('\ncurl command:')
|
||||
console.log(`curl -X POST http://localhost:8080/api/auth/login \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Signature: ${signature}" \\
|
||||
-H "X-Timestamp: ${timestamp}" \\
|
||||
-H "X-Nonce: ${nonce}" \\
|
||||
-d '${body}'`)
|
||||
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Novalon管理系统全面业务流程测试
|
||||
测试所有核心业务流程
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime
|
||||
from playwright.sync_api import sync_playwright, Page, expect
|
||||
|
||||
class TestResult:
|
||||
def __init__(self):
|
||||
self.total = 0
|
||||
self.passed = 0
|
||||
self.failed = 0
|
||||
self.errors = []
|
||||
self.start_time = datetime.now()
|
||||
|
||||
def add_pass(self, test_name):
|
||||
self.total += 1
|
||||
self.passed += 1
|
||||
print(f"✅ {test_name} - 通过")
|
||||
|
||||
def add_fail(self, test_name, error):
|
||||
self.total += 1
|
||||
self.failed += 1
|
||||
self.errors.append({"test": test_name, "error": str(error)})
|
||||
print(f"❌ {test_name} - 失败: {error}")
|
||||
|
||||
def print_summary(self):
|
||||
duration = (datetime.now() - self.start_time).total_seconds()
|
||||
print("\n" + "="*80)
|
||||
print("测试总结")
|
||||
print("="*80)
|
||||
print(f"总测试数: {self.total}")
|
||||
print(f"通过: {self.passed} ✅")
|
||||
print(f"失败: {self.failed} ❌")
|
||||
print(f"成功率: {(self.passed/self.total*100):.2f}%")
|
||||
print(f"耗时: {duration:.2f}秒")
|
||||
|
||||
if self.errors:
|
||||
print("\n失败详情:")
|
||||
for error in self.errors:
|
||||
print(f" - {error['test']}: {error['error']}")
|
||||
print("="*80)
|
||||
|
||||
result = TestResult()
|
||||
|
||||
def login(page: Page, username: str = "admin", password: str = "Test@123"):
|
||||
"""登录系统"""
|
||||
try:
|
||||
page.goto("http://localhost:3002/login")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
page.fill('input[placeholder*="用户名"]', username)
|
||||
page.fill('input[placeholder*="密码"]', password)
|
||||
page.click('button:has-text("登录")')
|
||||
|
||||
page.wait_for_url("**/dashboard", timeout=10000)
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"登录失败: {e}")
|
||||
return False
|
||||
|
||||
def test_user_management_flow(page: Page):
|
||||
"""测试用户管理完整流程"""
|
||||
print("\n📋 测试用户管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到用户管理页面
|
||||
page.click('text=用户管理')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试创建用户
|
||||
print(" - 测试创建用户...")
|
||||
page.click('button:has-text("新增")')
|
||||
time.sleep(0.5)
|
||||
|
||||
page.fill('input[placeholder*="用户名"]', f"testuser_{int(time.time())}")
|
||||
page.fill('input[placeholder*="密码"]', "admin123")
|
||||
page.fill('input[placeholder*="邮箱"]', f"test_{int(time.time())}@example.com")
|
||||
page.fill('input[placeholder*="手机"]', "13800138000")
|
||||
page.fill('input[placeholder*="昵称"]', "测试用户")
|
||||
|
||||
page.click('button:has-text("确定")')
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("用户管理-创建用户")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("用户管理-创建用户", e)
|
||||
|
||||
def test_role_management_flow(page: Page):
|
||||
"""测试角色管理完整流程"""
|
||||
print("\n📋 测试角色管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到角色管理页面
|
||||
page.click('text=角色管理')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试创建角色
|
||||
print(" - 测试创建角色...")
|
||||
page.click('button:has-text("新增")')
|
||||
time.sleep(0.5)
|
||||
|
||||
page.fill('input[placeholder*="角色名称"]', f"测试角色_{int(time.time())}")
|
||||
page.fill('input[placeholder*="角色标识"]', f"test_role_{int(time.time())}")
|
||||
|
||||
page.click('button:has-text("确定")')
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("角色管理-创建角色")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("角色管理-创建角色", e)
|
||||
|
||||
def test_menu_management_flow(page: Page):
|
||||
"""测试菜单管理完整流程"""
|
||||
print("\n📋 测试菜单管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到菜单管理页面
|
||||
page.click('text=菜单管理')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试创建菜单
|
||||
print(" - 测试创建菜单...")
|
||||
page.click('button:has-text("新增")')
|
||||
time.sleep(0.5)
|
||||
|
||||
page.fill('input[placeholder*="菜单名称"]', f"测试菜单_{int(time.time())}")
|
||||
page.select_option('select', 'C')
|
||||
|
||||
page.click('button:has-text("确定")')
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("菜单管理-创建菜单")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("菜单管理-创建菜单", e)
|
||||
|
||||
def test_dictionary_management_flow(page: Page):
|
||||
"""测试字典管理完整流程"""
|
||||
print("\n📋 测试字典管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到字典管理页面
|
||||
page.click('text=字典管理')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试查询字典
|
||||
print(" - 测试查询字典...")
|
||||
page.fill('input[placeholder*="搜索"]', "用户状态")
|
||||
page.press('input[placeholder*="搜索"]', 'Enter')
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("字典管理-查询字典")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("字典管理-查询字典", e)
|
||||
|
||||
def test_system_config_flow(page: Page):
|
||||
"""测试系统配置流程"""
|
||||
print("\n📋 测试系统配置流程...")
|
||||
|
||||
try:
|
||||
# 导航到系统配置页面
|
||||
page.click('text=系统配置')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试查询配置
|
||||
print(" - 测试查询配置...")
|
||||
page.fill('input[placeholder*="搜索"]', "用户")
|
||||
page.press('input[placeholder*="搜索"]', 'Enter')
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("系统配置-查询配置")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("系统配置-查询配置", e)
|
||||
|
||||
def test_file_management_flow(page: Page):
|
||||
"""测试文件管理流程"""
|
||||
print("\n📋 测试文件管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到文件管理页面
|
||||
page.click('text=文件管理')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试查看文件列表
|
||||
print(" - 测试查看文件列表...")
|
||||
page.wait_for_selector('table', timeout=5000)
|
||||
|
||||
result.add_pass("文件管理-查看文件列表")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("文件管理-查看文件列表", e)
|
||||
|
||||
def test_notification_flow(page: Page):
|
||||
"""测试通知管理流程"""
|
||||
print("\n📋 测试通知管理流程...")
|
||||
|
||||
try:
|
||||
# 导航到通知管理页面
|
||||
page.click('text=通知公告')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试查看通知列表
|
||||
print(" - 测试查看通知列表...")
|
||||
page.wait_for_selector('table', timeout=5000)
|
||||
|
||||
result.add_pass("通知管理-查看通知列表")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("通知管理-查看通知列表", e)
|
||||
|
||||
def test_audit_log_flow(page: Page):
|
||||
"""测试审计日志流程"""
|
||||
print("\n📋 测试审计日志流程...")
|
||||
|
||||
try:
|
||||
# 导航到操作日志页面
|
||||
page.click('text=操作日志')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 测试查看日志列表
|
||||
print(" - 测试查看操作日志...")
|
||||
page.wait_for_selector('table', timeout=5000)
|
||||
|
||||
result.add_pass("审计日志-查看操作日志")
|
||||
|
||||
# 导航到登录日志页面
|
||||
page.click('text=登录日志')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
result.add_pass("审计日志-查看登录日志")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("审计日志-查看日志", e)
|
||||
|
||||
def test_permission_validation(page: Page):
|
||||
"""测试权限验证"""
|
||||
print("\n📋 测试权限验证...")
|
||||
|
||||
try:
|
||||
# 测试菜单权限
|
||||
print(" - 测试菜单访问权限...")
|
||||
menus = ['用户管理', '角色管理', '菜单管理', '字典管理', '系统配置']
|
||||
|
||||
for menu in menus:
|
||||
try:
|
||||
page.click(f'text={menu}')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(0.5)
|
||||
result.add_pass(f"权限验证-访问{menu}")
|
||||
except:
|
||||
result.add_fail(f"权限验证-访问{menu}", "无法访问")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("权限验证", e)
|
||||
|
||||
def test_dashboard(page: Page):
|
||||
"""测试仪表板"""
|
||||
print("\n📋 测试仪表板...")
|
||||
|
||||
try:
|
||||
# 导航到仪表板
|
||||
page.click('text=仪表板')
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(1)
|
||||
|
||||
# 验证仪表板加载
|
||||
page.wait_for_selector('.dashboard-container, .stats-card, .chart', timeout=5000)
|
||||
|
||||
result.add_pass("仪表板-加载成功")
|
||||
|
||||
except Exception as e:
|
||||
result.add_fail("仪表板-加载", e)
|
||||
|
||||
def main():
|
||||
print("="*80)
|
||||
print("Novalon管理系统全面业务流程测试")
|
||||
print("="*80)
|
||||
print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("="*80)
|
||||
|
||||
with sync_playwright() as p:
|
||||
# 启动浏览器
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
|
||||
try:
|
||||
# 登录测试
|
||||
print("\n🔐 测试登录...")
|
||||
if login(page):
|
||||
result.add_pass("登录功能")
|
||||
print("✅ 登录成功")
|
||||
else:
|
||||
result.add_fail("登录功能", "登录失败")
|
||||
return
|
||||
|
||||
# 测试仪表板
|
||||
test_dashboard(page)
|
||||
|
||||
# 测试用户管理流程
|
||||
test_user_management_flow(page)
|
||||
|
||||
# 测试角色管理流程
|
||||
test_role_management_flow(page)
|
||||
|
||||
# 测试菜单管理流程
|
||||
test_menu_management_flow(page)
|
||||
|
||||
# 测试字典管理流程
|
||||
test_dictionary_management_flow(page)
|
||||
|
||||
# 测试系统配置流程
|
||||
test_system_config_flow(page)
|
||||
|
||||
# 测试文件管理流程
|
||||
test_file_management_flow(page)
|
||||
|
||||
# 测试通知管理流程
|
||||
test_notification_flow(page)
|
||||
|
||||
# 测试审计日志流程
|
||||
test_audit_log_flow(page)
|
||||
|
||||
# 测试权限验证
|
||||
test_permission_validation(page)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试执行出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
# 打印测试总结
|
||||
result.print_summary()
|
||||
|
||||
# 返回退出码
|
||||
return 0 if result.failed == 0 else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
|
||||
SECRET = 'NovalonManageSystemSecretKey2026'
|
||||
|
||||
def generate_signature(method, path, query='', body='', timestamp=None, nonce=None):
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time() * 1000)
|
||||
if nonce is None:
|
||||
nonce = f"{int(timestamp)}-{hash(time.time())}"
|
||||
|
||||
string_to_sign = f"{method}\n{path}\n{query}\n{body}\n{timestamp}\n{nonce}"
|
||||
|
||||
signature = hmac.new(
|
||||
SECRET.encode('utf-8'),
|
||||
string_to_sign.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).digest()
|
||||
|
||||
signature_base64 = base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
return signature_base64, timestamp, nonce
|
||||
|
||||
method = 'POST'
|
||||
path = '/api/auth/login'
|
||||
body = ''
|
||||
|
||||
signature, timestamp, nonce = generate_signature(method, path, body=body)
|
||||
|
||||
print(f"X-Signature: {signature}")
|
||||
print(f"X-Timestamp: {timestamp}")
|
||||
print(f"X-Nonce: {nonce}")
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Signature': signature,
|
||||
'X-Timestamp': str(timestamp),
|
||||
'X-Nonce': nonce
|
||||
}
|
||||
|
||||
response = requests.post('http://localhost:8080/api/auth/login',
|
||||
headers=headers,
|
||||
data='{"username":"admin","password":"admin123"}',
|
||||
verify=False)
|
||||
|
||||
print(f"\nResponse Status: {response.status_code}")
|
||||
print(f"Response Body: {response.text}")
|
||||
Reference in New Issue
Block a user