feat(api/web): 实现API请求签名验证功能并优化测试环境配置

refactor(db): 重构查询条件类到query目录下

test: 添加登录流程测试脚本和测试数据

chore: 添加crypto-js依赖用于签名验证

ci: 配置测试环境数据库和端口设置
This commit is contained in:
张翔
2026-04-02 08:07:53 +08:00
parent 1e3dc11d59
commit 6392c08560
40 changed files with 1679 additions and 800 deletions
@@ -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,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,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,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,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,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,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,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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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}
@@ -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;
/**
* 审计日志切面
@@ -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;
/**
* 审计日志领域对象
@@ -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;
/**
* 异步配置类
@@ -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审计配置类
@@ -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();
}
}
@@ -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));
}
}
@@ -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));
}
}