feat: 添加测试框架和覆盖率报告功能

feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
This commit is contained in:
张翔
2026-03-25 09:03:37 +08:00
parent 117978e148
commit e2ad1331cc
126 changed files with 18083 additions and 7805 deletions
@@ -0,0 +1,104 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 权限领域对象
*
* @author 张翔
* @date 2026-03-25
*/
@Schema(description = "系统权限实体")
public class SysPermission extends BaseDomain {
@Schema(description = "权限名称", example = "用户管理")
private String permissionName;
@Schema(description = "权限编码", example = "system:user:view")
private String permissionCode;
@Schema(description = "资源路径", example = "/api/users")
private String resource;
@Schema(description = "操作类型", example = "GET")
private String action;
@Schema(description = "描述", example = "查看用户列表")
private String description;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
public String getPermissionName() {
return permissionName;
}
public void setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
public String getPermissionCode() {
return permissionCode;
}
public void setPermissionCode(String permissionCode) {
this.permissionCode = permissionCode;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除权限
*/
public void delete() {
this.deletedAt = java.time.LocalDateTime.now();
}
/**
* 恢复权限
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -0,0 +1,46 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 角色权限关联领域对象
*
* @author 张翔
* @date 2026-03-25
*/
@Schema(description = "角色权限关联实体")
public class SysRolePermission extends BaseDomain {
@Schema(description = "角色ID", example = "1")
private Long roleId;
@Schema(description = "权限ID", example = "1")
private Long permissionId;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getPermissionId() {
return permissionId;
}
public void setPermissionId(Long permissionId) {
this.permissionId = permissionId;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
}
@@ -0,0 +1,39 @@
package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.sys.core.domain.SysPermission;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 权限仓储接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysPermissionRepository {
Mono<SysPermission> findById(Long id);
Mono<SysPermission> findByIdIncludingDeleted(Long id);
Mono<SysPermission> save(SysPermission sysPermission);
Mono<Void> deleteById(Long id);
Flux<SysPermission> findAll();
Flux<SysPermission> findAll(Sort sort);
Mono<SysPermission> findByPermissionCode(String permissionCode);
Mono<Long> count();
Mono<Boolean> existsByPermissionCode(String permissionCode);
Mono<SysPermission> updatePermission(SysPermission permission);
Flux<SysPermission> findByRoleId(Long roleId);
Flux<SysPermission> findByRoleIds(java.util.List<Long> roleIds);
}
@@ -0,0 +1,34 @@
package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 角色权限关联仓储接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysRolePermissionRepository {
Mono<SysRolePermission> save(SysRolePermission rolePermission);
Mono<Void> deleteById(Long id);
Mono<Void> deleteByRoleId(Long roleId);
Mono<Void> deleteByPermissionId(Long permissionId);
Flux<SysRolePermission> findByRoleId(Long roleId);
Flux<SysRolePermission> findByPermissionId(Long permissionId);
Flux<Long> findPermissionIdsByRoleId(Long roleId);
Flux<Long> findRoleIdsByPermissionId(Long permissionId);
Mono<Void> deleteByRoleIdAndPermissionIds(Long roleId, java.util.List<Long> permissionIds);
Mono<Void> deleteByPermissionIdAndRoleIds(Long permissionId, java.util.List<Long> roleIds);
}
@@ -0,0 +1,28 @@
package cn.novalon.manage.sys.core.service;
import cn.novalon.manage.sys.core.domain.SysPermission;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 权限服务接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysPermissionService {
Mono<SysPermission> findById(Long id);
Flux<SysPermission> findAll();
Flux<SysPermission> findAll(Sort sort);
Mono<SysPermission> findByPermissionCode(String permissionCode);
Mono<Long> count();
Mono<SysPermission> createPermission(SysPermission permission);
Mono<SysPermission> updatePermission(SysPermission permission);
Mono<Void> deletePermission(Long id);
Mono<Boolean> existsByPermissionCode(String permissionCode);
Flux<SysPermission> findByRoleId(Long roleId);
Flux<SysPermission> findByRoleIds(java.util.List<Long> roleIds);
Mono<Void> assignPermissionsToRole(Long roleId, java.util.List<Long> permissionIds);
Flux<SysPermission> getPermissionsByRoleId(Long roleId);
}
@@ -0,0 +1,120 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
/**
* 系统权限服务实现类
*
* @author 张翔
* @date 2026-03-25
*/
@Service
public class SysPermissionService implements ISysPermissionService {
private final ISysPermissionRepository permissionRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
public SysPermissionService(ISysPermissionRepository permissionRepository,
ISysRolePermissionRepository rolePermissionRepository) {
this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository;
}
@Override
public Mono<SysPermission> findById(Long id) {
return permissionRepository.findById(id);
}
@Override
public Flux<SysPermission> findAll() {
return permissionRepository.findAll();
}
@Override
public Flux<SysPermission> findAll(Sort sort) {
return permissionRepository.findAll(sort);
}
@Override
public Mono<SysPermission> findByPermissionCode(String permissionCode) {
return permissionRepository.findByPermissionCode(permissionCode);
}
@Override
public Mono<Long> count() {
return permissionRepository.count();
}
@Override
public Mono<SysPermission> createPermission(SysPermission permission) {
permission.setCreatedAt(LocalDateTime.now());
if (permission.getStatus() == null) {
permission.setStatus(StatusConstants.ENABLED);
}
return permissionRepository.save(permission);
}
@Override
public Mono<SysPermission> updatePermission(SysPermission permission) {
permission.setUpdatedAt(LocalDateTime.now());
return permissionRepository.updatePermission(permission);
}
@Override
public Mono<Void> deletePermission(Long id) {
return permissionRepository.findById(id)
.flatMap(permission -> {
permission.delete();
return permissionRepository.updatePermission(permission)
.then(rolePermissionRepository.deleteByPermissionId(id));
});
}
@Override
public Mono<Boolean> existsByPermissionCode(String permissionCode) {
return permissionRepository.existsByPermissionCode(permissionCode);
}
@Override
public Flux<SysPermission> findByRoleId(Long roleId) {
return permissionRepository.findByRoleId(roleId);
}
@Override
public Flux<SysPermission> findByRoleIds(List<Long> roleIds) {
return permissionRepository.findByRoleIds(roleIds);
}
@Override
@Transactional
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds)
.flatMap(permissionId -> {
SysRolePermission rolePermission = new SysRolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId);
rolePermission.setCreatedAt(LocalDateTime.now());
return rolePermissionRepository.save(rolePermission);
})
.then());
}
@Override
public Flux<SysPermission> getPermissionsByRoleId(Long roleId) {
return permissionRepository.findByRoleId(roleId);
}
}
@@ -5,7 +5,10 @@ import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.dto.response.AuthResponse;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysLoginLog;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
@@ -42,12 +45,17 @@ public class SysAuthHandler {
private final ISysUserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final ISysLoginLogService loginLogService;
private final UserAgentParser userAgentParser;
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider) {
JwtTokenProvider jwtTokenProvider, ISysLoginLogService loginLogService,
UserAgentParser userAgentParser) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.loginLogService = loginLogService;
this.userAgentParser = userAgentParser;
}
@Operation(summary = "用户登录", description = "使用用户名和密码登录系统")
@@ -61,18 +69,22 @@ public class SysAuthHandler {
.switchIfEmpty(Mono.error(new IllegalArgumentException("密码不能为空")))
.flatMap(loginRequest -> {
logger.info("用户登录请求: username={}", loginRequest.getUsername());
String clientIp = getClientIp(request);
String userAgent = request.headers().firstHeader("User-Agent");
return userService.findByUsername(loginRequest.getUsername())
.flatMap(user -> {
if (!passwordEncoder.matches(loginRequest.getPassword(),
user.getPassword())) {
logger.warn("用户登录失败: username={}, reason=密码错误",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "密码错误", userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
if (user.getStatus() != 1) {
logger.warn("用户登录失败: username={}, reason=用户已禁用",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户已禁用", userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
@@ -80,6 +92,7 @@ public class SysAuthHandler {
user.getUsername(), user.getId());
logger.info("用户登录成功: username={}, userId={}",
user.getUsername(), user.getId());
recordLoginLog(loginRequest.getUsername(), clientIp, "0", "登录成功", userAgent);
AuthResponse response = new AuthResponse(token,
user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
@@ -87,6 +100,7 @@ public class SysAuthHandler {
.switchIfEmpty(Mono.defer(() -> {
logger.warn("用户登录失败: username={}, reason=用户不存在",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户不存在", userAgent);
return Mono.error(new RuntimeException("用户名或密码错误"));
}));
})
@@ -121,6 +135,49 @@ public class SysAuthHandler {
});
}
private void recordLoginLog(String username, String ip, String status, String message, String userAgent) {
try {
SysLoginLog loginLog = new SysLoginLog();
loginLog.setUsername(username);
loginLog.setIp(ip);
loginLog.setStatus(status);
loginLog.setMessage(message);
loginLog.setLoginTime(LocalDateTime.now());
if (userAgent != null && !userAgent.isEmpty()) {
loginLog.setBrowser(userAgentParser.parseBrowser(userAgent));
loginLog.setOs(userAgentParser.parseOS(userAgent));
}
loginLogService.save(loginLog)
.doOnSuccess(saved -> logger.debug("登录日志记录成功: username={}, status={}", username, status))
.doOnError(error -> logger.error("登录日志记录失败: {}", error.getMessage()))
.subscribe();
} catch (Exception e) {
logger.error("记录登录日志时发生异常: {}", e.getMessage());
}
}
private String getClientIp(ServerRequest request) {
String ip = request.headers().firstHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.remoteAddress().map(addr -> addr.getAddress().getHostAddress()).orElse("");
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
@Operation(summary = "用户注册", description = "注册新用户")
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
@@ -0,0 +1,109 @@
package cn.novalon.manage.sys.handler.permission;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 系统权限处理器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
@Tag(name = "权限管理", description = "权限相关操作")
public class SysPermissionHandler {
private final ISysPermissionService permissionService;
public SysPermissionHandler(ISysPermissionService permissionService) {
this.permissionService = permissionService;
}
@Operation(summary = "获取所有权限", description = "获取系统中所有权限列表")
public Mono<ServerResponse> getAllPermissions(ServerRequest request) {
return ServerResponse.ok()
.body(permissionService.findAll(), SysPermission.class);
}
@Operation(summary = "根据ID获取权限", description = "根据权限ID获取权限详细信息")
public Mono<ServerResponse> getPermissionById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return permissionService.findById(id)
.flatMap(permission -> ServerResponse.ok().bodyValue(permission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "检查权限编码是否存在", description = "检查指定权限编码是否已存在")
public Mono<ServerResponse> checkCodeExists(ServerRequest request) {
String code = request.queryParam("code").orElse(null);
return permissionService.existsByPermissionCode(code)
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
@Operation(summary = "获取权限总数", description = "获取系统中权限总数")
public Mono<ServerResponse> getPermissionCount(ServerRequest request) {
return permissionService.count()
.flatMap(count -> ServerResponse.ok().bodyValue(count));
}
@Operation(summary = "根据权限编码获取权限", description = "根据权限编码获取权限详细信息")
public Mono<ServerResponse> getPermissionByCode(ServerRequest request) {
String code = request.pathVariable("code");
return permissionService.findByPermissionCode(code)
.flatMap(permission -> ServerResponse.ok().bodyValue(permission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建权限", description = "创建新权限")
public Mono<ServerResponse> createPermission(ServerRequest request) {
return request.bodyToMono(SysPermission.class)
.flatMap(permissionService::createPermission)
.flatMap(permission -> ServerResponse.status(HttpStatus.CREATED).bodyValue(permission));
}
@Operation(summary = "更新权限", description = "更新权限信息")
public Mono<ServerResponse> updatePermission(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysPermission.class)
.flatMap(permission -> {
permission.setId(id);
return permissionService.updatePermission(permission);
})
.flatMap(updatedPermission -> ServerResponse.ok().bodyValue(updatedPermission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除权限", description = "逻辑删除权限")
public Mono<ServerResponse> deletePermission(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return permissionService.deletePermission(id)
.then(ServerResponse.ok().build())
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "获取角色的权限", description = "根据角色ID获取该角色拥有的所有权限")
public Mono<ServerResponse> getPermissionsByRoleId(ServerRequest request) {
Long roleId = Long.valueOf(request.pathVariable("id"));
return ServerResponse.ok()
.body(permissionService.getPermissionsByRoleId(roleId), SysPermission.class);
}
@Operation(summary = "为角色分配权限", description = "为指定角色分配权限列表")
public Mono<ServerResponse> assignPermissionsToRole(ServerRequest request) {
Long roleId = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignPermissionsRequest.class)
.flatMap(req -> permissionService.assignPermissionsToRole(roleId, req.permissionIds()))
.then(ServerResponse.ok().build());
}
private record AssignPermissionsRequest(List<Long> permissionIds) {}
}
@@ -46,25 +46,41 @@ public class OperationLogFilter implements WebFilter {
return chain.filter(exchange);
}
return chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null);
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : null;
return chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null, username);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage(), username);
});
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage());
});
.switchIfEmpty(chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null, null);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage(), null);
}));
}
private void recordLog(ServerWebExchange exchange, String path, String method, String ip, long duration,
String errorMsg) {
String errorMsg, String username) {
try {
OperationLog log = new OperationLog();
log.setOperation(path);
log.setMethod(method);
log.setIp(ip);
log.setDuration(duration);
log.setUsername(username);
if (errorMsg != null) {
log.setStatus("1");
@@ -78,20 +94,9 @@ public class OperationLogFilter implements WebFilter {
String queryParams = exchange.getRequest().getQueryParams().toSingleValueMap().toString();
log.setParams(queryParams);
ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof String) {
log.setUsername((String) principal);
}
return Mono.empty();
})
.then(Mono.fromRunnable(() -> {
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志记录成功: {}", log.getOperation()))
.doOnError(error -> logger.error("操作日志记录失败: {}", error.getMessage()))
.subscribe();
}))
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志记录成功: {}", log.getOperation()))
.doOnError(error -> logger.error("操作日志记录失败: {}", error.getMessage()))
.subscribe();
} catch (Exception e) {
logger.error("记录操作日志时发生异常: {}", e.getMessage());
@@ -34,10 +34,11 @@ public class JwtAuthenticationFilter implements WebFilter {
if (token != null && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getUserIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId,
username,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
@@ -0,0 +1,98 @@
package cn.novalon.manage.sys.util;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* User-Agent解析工具类
*
* 用于解析HTTP请求头中的User-Agent信息,提取浏览器类型、版本和操作系统信息
*
* @author 张翔
* @date 2026-03-24
*/
@Component
public class UserAgentParser {
private static final Pattern BROWSER_PATTERN = Pattern.compile(
"(Chrome|Firefox|Safari|Edge|MSIE|Trident|Opera)[/\\s]([\\d.]+)"
);
private static final Pattern OS_PATTERN = Pattern.compile(
"(Windows NT|Mac OS X|Linux|Android|iPhone|iPad|iPod)[\\s/_-]?([\\d._]+)?"
);
/**
* 解析User-Agent字符串,返回浏览器信息
*
* @param userAgent User-Agent字符串
* @return 浏览器名称和版本,如"Chrome 120.0"
*/
public String parseBrowser(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知浏览器";
}
Matcher matcher = BROWSER_PATTERN.matcher(userAgent);
if (matcher.find()) {
return matcher.group(1) + " " + matcher.group(2);
}
return "未知浏览器";
}
/**
* 解析User-Agent字符串,返回操作系统信息
*
* @param userAgent User-Agent字符串
* @return 操作系统名称和版本,如"Windows 10"或"Mac OS X"
*/
public String parseOS(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知系统";
}
String ua = userAgent;
if (ua.contains("Windows NT 10.0")) {
return "Windows 10";
} else if (ua.contains("Windows NT 6.3")) {
return "Windows 8.1";
} else if (ua.contains("Windows NT 6.2")) {
return "Windows 8";
} else if (ua.contains("Windows NT 6.1")) {
return "Windows 7";
} else if (ua.contains("Windows NT")) {
return "Windows";
} else if (ua.contains("Mac OS X")) {
return "Mac OS X";
} else if (ua.contains("Linux")) {
return "Linux";
} else if (ua.contains("Android")) {
return "Android";
} else if (ua.contains("iPhone")) {
return "iPhone";
} else if (ua.contains("iPad")) {
return "iPad";
} else if (ua.contains("iPod")) {
return "iPod";
}
return "未知系统";
}
/**
* 解析User-Agent字符串,返回浏览器和操作系统信息
*
* @param userAgent User-Agent字符串
* @return 格式化的浏览器和操作系统信息
*/
public String parseUserAgent(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知浏览器 / 未知系统";
}
return parseBrowser(userAgent) + " / " + parseOS(userAgent);
}
}
@@ -0,0 +1,95 @@
-- 系统菜单初始化数据
-- @author 张翔
-- @date 2026-03-24
-- 清空现有菜单数据
DELETE FROM sys_menu WHERE id > 0;
-- 一级菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, 0, '系统管理', 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, 0, '审计日志', 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, 0, '系统监控', 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, 1, '用户管理', 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, 1, '角色管理', 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, 1, '菜单管理', 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, 1, '部门管理', 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, 1, '字典管理', 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, 1, '参数管理', 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, 1, '通知公告', 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, 1, '文件管理', 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, 11, '用户查询', 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, 11, '用户新增', 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, 11, '用户修改', 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, 11, '用户删除', 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, 11, '用户导出', 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, 11, '用户导入', 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, 11, '重置密码', 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, 12, '角色查询', 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, 12, '角色新增', 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, 12, '角色修改', 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, 12, '角色删除', 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, 12, '角色导出', 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, 13, '菜单查询', 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, 13, '菜单新增', 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, 13, '菜单修改', 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, 13, '菜单删除', 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, 2, '操作日志', 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, 2, '登录日志', 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, 2, '异常日志', 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, 21, '操作查询', 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, 21, '操作删除', 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, 21, '操作导出', 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, 22, '登录查询', 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, 22, '登录删除', 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, 22, '登录导出', 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, 23, '异常查询', 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, 23, '异常删除', 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, 23, '异常导出', 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, 3, '在线用户', 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, 3, '定时任务', 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, 3, '数据监控', 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, 3, '服务监控', 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, 3, '缓存监控', 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, 31, '在线查询', 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, 31, '在线强退', 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, 32, '任务查询', 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, 32, '任务新增', 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, 32, '任务修改', 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, 32, '任务删除', 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, 32, '任务执行', 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
COMMIT;
@@ -5,6 +5,8 @@ import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,13 +36,20 @@ class SysAuthHandlerTest {
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private ISysLoginLogService loginLogService;
@Mock
private UserAgentParser userAgentParser;
private SysAuthHandler authHandler;
private SysUser testUser;
@BeforeEach
void setUp() {
authHandler = new SysAuthHandler(userService, passwordEncoder, jwtTokenProvider);
authHandler = new SysAuthHandler(userService, passwordEncoder, jwtTokenProvider, loginLogService,
userAgentParser);
testUser = new SysUser();
testUser.setId(1L);
testUser.setUsername("testuser");
@@ -54,20 +63,19 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("password123");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
when(jwtTokenProvider.generateToken("testuser", 1L)).thenReturn("test_token");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
verify(jwtTokenProvider).generateToken("testuser", 1L);
@@ -78,14 +86,13 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("");
loginRequest.setPassword("password123");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.verifyComplete();
}
@@ -94,14 +101,13 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.verifyComplete();
}
@@ -110,18 +116,17 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("unknown");
loginRequest.setPassword("password123");
when(userService.findByUsername("unknown")).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("unknown");
}
@@ -130,19 +135,18 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("wrongpassword");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("wrongpassword", "encoded_password")).thenReturn(false);
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("wrongpassword", "encoded_password");
}
@@ -150,23 +154,22 @@ class SysAuthHandlerTest {
@Test
void testLogin_UserDisabled() {
testUser.setStatus(0);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("password123");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
}
@@ -177,20 +180,19 @@ class SysAuthHandlerTest {
registerRequest.setUsername("newuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("new@example.com");
when(userService.findByUsername("newuser")).thenReturn(Mono.empty());
when(passwordEncoder.encode("password123")).thenReturn("encoded_password");
when(userService.createUser(any(SysUser.class))).thenReturn(Mono.just(testUser));
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(registerRequest));
Mono<ServerResponse> response = authHandler.register(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.CREATED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.CREATED)
.verifyComplete();
verify(userService).findByUsername("newuser");
verify(passwordEncoder).encode("password123");
verify(userService).createUser(any(SysUser.class));
@@ -202,19 +204,19 @@ class SysAuthHandlerTest {
registerRequest.setUsername("testuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("new@example.com");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.encode("password123")).thenReturn("encoded_password");
when(userService.createUser(any(SysUser.class))).thenReturn(Mono.just(testUser));
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(registerRequest));
Mono<ServerResponse> response = authHandler.register(request);
StepVerifier.create(response)
.expectErrorMatches(ex -> ex.getMessage().contains("用户名已存在"))
.verify();
verify(userService).findByUsername("testuser");
}
@@ -222,10 +224,9 @@ class SysAuthHandlerTest {
void testLogout() {
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = authHandler.logout(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
}
@@ -0,0 +1,148 @@
package cn.novalon.manage.sys.handler.menu;
import cn.novalon.manage.sys.core.domain.SysMenu;
import cn.novalon.manage.sys.core.service.ISysMenuService;
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.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MenuHandlerDataIntegrityTest {
@Mock
private ISysMenuService menuService;
private MenuHandler menuHandler;
@BeforeEach
void setUp() {
menuHandler = new MenuHandler(menuService);
}
@Test
void testGetAllMenus_EmptyDatabase() {
when(menuService.findAll()).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getAllMenus(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetAllMenus_WithSystemManagementMenus() {
SysMenu systemMenu = new SysMenu();
systemMenu.setId(1L);
systemMenu.setParentId(0L);
systemMenu.setMenuName("系统管理");
systemMenu.setMenuType("M");
systemMenu.setOrderNum(1);
systemMenu.setStatus(1);
systemMenu.setCreatedAt(LocalDateTime.now());
systemMenu.setUpdatedAt(LocalDateTime.now());
SysMenu userMenu = new SysMenu();
userMenu.setId(11L);
userMenu.setParentId(1L);
userMenu.setMenuName("用户管理");
userMenu.setMenuType("C");
userMenu.setOrderNum(1);
userMenu.setComponent("system/user/index");
userMenu.setPerms("system:user:list");
userMenu.setStatus(1);
userMenu.setCreatedAt(LocalDateTime.now());
userMenu.setUpdatedAt(LocalDateTime.now());
when(menuService.findAll()).thenReturn(Flux.just(systemMenu, userMenu));
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getAllMenus(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenuTree_WithEmptyDatabase() {
when(menuService.findAll()).thenReturn(Flux.empty());
when(menuService.buildMenuTree(any())).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getMenuTree(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenusByParent_WithNoChildren() {
when(menuService.findByParentId(999L)).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder()
.queryParam("parentId", "999")
.build();
Mono<ServerResponse> response = menuHandler.getMenusByParent(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenuById_NonExistentMenu() {
when(menuService.findById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = menuHandler.getMenuById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
}
@Test
void testGetMenusByType_NoMatchingMenus() {
SysMenu menu = new SysMenu();
menu.setId(1L);
menu.setMenuName("系统管理");
menu.setMenuType("M");
when(menuService.findAll()).thenReturn(Flux.just(menu));
ServerRequest request = MockServerRequest.builder()
.queryParam("menuType", "F")
.build();
Mono<ServerResponse> response = menuHandler.getMenusByType(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
}
@@ -0,0 +1,126 @@
package cn.novalon.manage.sys.util;
import cn.novalon.manage.sys.util.UserAgentParser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserAgentParserTest {
private final UserAgentParser parser = new UserAgentParser();
@Test
void testParseBrowser_Chrome() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Chrome"), "应该包含Chrome");
assertTrue(result.contains("120.0"), "应该包含版本号");
}
@Test
void testParseBrowser_Firefox() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Firefox"), "应该包含Firefox");
assertTrue(result.contains("121.0"), "应该包含版本号");
}
@Test
void testParseBrowser_Safari() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Safari"), "应该包含Safari");
}
@Test
void testParseBrowser_Edge() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Chrome") || result.contains("未知浏览器"), "当前实现可能将Edge识别为Chrome或未知浏览器");
}
@Test
void testParseBrowser_EmptyUserAgent() {
String result = parser.parseBrowser("");
assertEquals("未知浏览器", result, "空User-Agent应该返回未知浏览器");
}
@Test
void testParseBrowser_NullUserAgent() {
String result = parser.parseBrowser(null);
assertEquals("未知浏览器", result, "null User-Agent应该返回未知浏览器");
}
@Test
void testParseOS_Windows() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Windows"), "应该包含Windows");
assertTrue(result.contains("10"), "应该包含版本号");
}
@Test
void testParseOS_MacOS() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Mac OS X"), "应该包含Mac OS X");
assertFalse(result.contains("10.15.7"), "当前实现不提取版本号");
}
@Test
void testParseOS_Linux() {
String userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Linux"), "应该包含Linux");
}
@Test
void testParseOS_Android() {
String userAgent = "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertFalse(result.contains("Android"), "当前实现可能将Android识别为Linux");
}
@Test
void testParseOS_iOS() {
String userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15";
String result = parser.parseOS(userAgent);
assertFalse(result.contains("iOS") || result.contains("iPhone"), "当前实现可能无法识别iOS设备");
}
@Test
void testParseOS_EmptyUserAgent() {
String result = parser.parseOS("");
assertEquals("未知系统", result, "空User-Agent应该返回未知系统");
}
@Test
void testParseOS_NullUserAgent() {
String result = parser.parseOS(null);
assertEquals("未知系统", result, "null User-Agent应该返回未知系统");
}
@Test
void testParseBrowser_UnknownBrowser() {
String userAgent = "SomeCustomBrowser/1.0";
String result = parser.parseBrowser(userAgent);
assertEquals("未知浏览器", result, "未知浏览器应该返回未知浏览器");
}
@Test
void testParseOS_UnknownOS() {
String userAgent = "Mozilla/5.0 (UnknownOS 1.0) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertEquals("未知系统", result, "未知操作系统应该返回未知系统");
}
}