feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,16 @@
package io.destiny;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ConfigurationPropertiesScan(basePackages = "io.destiny")
@ComponentScan(basePackages = "io.destiny")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,29 @@
package io.destiny.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
@@ -0,0 +1,53 @@
package io.destiny.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DatabaseClient databaseClient;
public DatabaseHealthIndicator(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}
@Override
public Health health() {
return checkDatabase()
.map(status -> {
if (status) {
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("status", "Connected")
.build();
} else {
return Health.down()
.withDetail("database", "PostgreSQL")
.withDetail("status", "Disconnected")
.withDetail("reason", "Failed to connect to database")
.build();
}
})
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> Mono.just(Health.down()
.withDetail("database", "PostgreSQL")
.withDetail("status", "Error")
.withDetail("reason", e.getMessage())
.build()))
.block();
}
private Mono<Boolean> checkDatabase() {
return databaseClient.sql("SELECT 1")
.fetch()
.one()
.map(result -> true)
.onErrorResume(e -> Mono.just(false));
}
}
@@ -0,0 +1,47 @@
package io.destiny.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.io.File;
@Component
public class DiskSpaceHealthIndicator implements HealthIndicator {
private static final long MIN_FREE_SPACE = 100 * 1024 * 1024; // 100MB
@Override
public Health health() {
File root = new File("/");
long freeSpace = root.getFreeSpace();
long totalSpace = root.getTotalSpace();
long usableSpace = root.getUsableSpace();
Health.Builder builder = Health.up();
builder.withDetail("freeSpace", formatBytes(freeSpace))
.withDetail("totalSpace", formatBytes(totalSpace))
.withDetail("usableSpace", formatBytes(usableSpace))
.withDetail("freeSpacePercent", String.format("%.2f%%", (double) freeSpace / totalSpace * 100));
if (freeSpace < MIN_FREE_SPACE) {
builder = Health.down();
builder.withDetail("reason", "Disk space is running low");
}
return builder.build();
}
private String formatBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.2f KB", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024));
} else {
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
}
}
@@ -0,0 +1,88 @@
package io.destiny.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
@Component
public class SystemResourceHealthIndicator implements HealthIndicator {
private final OperatingSystemMXBean osBean;
private final MemoryMXBean memoryBean;
public SystemResourceHealthIndicator() {
this.osBean = ManagementFactory.getOperatingSystemMXBean();
this.memoryBean = ManagementFactory.getMemoryMXBean();
}
@Override
public Health health() {
Health.Builder builder = Health.up();
double systemCpuLoad = getSystemCpuLoad();
double processCpuLoad = getProcessCpuLoad();
long usedMemory = getUsedMemory();
long maxMemory = getMaxMemory();
double memoryUsagePercent = (double) usedMemory / maxMemory * 100;
builder.withDetail("systemCpuLoad", String.format("%.2f%%", systemCpuLoad))
.withDetail("processCpuLoad", String.format("%.2f%%", processCpuLoad))
.withDetail("usedMemory", formatBytes(usedMemory))
.withDetail("maxMemory", formatBytes(maxMemory))
.withDetail("memoryUsagePercent", String.format("%.2f%%", memoryUsagePercent))
.withDetail("availableProcessors", osBean.getAvailableProcessors())
.withDetail("systemLoadAverage", osBean.getSystemLoadAverage());
if (systemCpuLoad > 90) {
builder = Health.down();
builder.withDetail("reason", "System CPU load is too high");
} else if (memoryUsagePercent > 90) {
builder = Health.down();
builder.withDetail("reason", "Memory usage is too high");
}
return builder.build();
}
private double getSystemCpuLoad() {
double systemCpuLoad = osBean.getSystemLoadAverage();
if (systemCpuLoad < 0) {
return 0;
}
return (systemCpuLoad / osBean.getAvailableProcessors()) * 100;
}
private double getProcessCpuLoad() {
try {
com.sun.management.OperatingSystemMXBean sunOsBean =
(com.sun.management.OperatingSystemMXBean) osBean;
return sunOsBean.getProcessCpuLoad() * 100;
} catch (Exception e) {
return 0;
}
}
private long getUsedMemory() {
return memoryBean.getHeapMemoryUsage().getUsed();
}
private long getMaxMemory() {
return memoryBean.getHeapMemoryUsage().getMax();
}
private String formatBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.2f KB", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024));
} else {
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
}
}
@@ -0,0 +1,34 @@
server:
port: 8080
spring:
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:ziwei_destiny_db}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:123456}
pool:
initial-size: 10
max-size: 30
max-idle-time: 10m
max-life-time: 30m
max-create-connection-time: 5s
flyway:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:ziwei_destiny_db}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:123456}
webflux:
base-path: /api
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
gateway:
public-paths: /client/auth/register,/client/auth/login,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/swagger-ui/**,/webjars/**,/actuator/health,/actuator/info
logging:
level:
root: INFO
"[io.destiny]": DEBUG
@@ -0,0 +1,44 @@
server:
port: 8080
spring:
cloud:
compatibility-verifier:
enabled: false
r2dbc:
url: r2dbc:postgresql://postgres:postgres123@127.0.0.1:5432/everything_is_suitable
pool:
initial-size: 5
max-size: 20
max-idle-time: 10m
max-life-time: 30m
max-create-connection-time: 5s
flyway:
url: jdbc:postgresql://127.0.0.1:5432/everything_is_suitable
username: postgres
password: postgres123
validate-on-migrate: false
webflux:
base-path: /api
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
gateway:
public-paths: /swagger-ui,/swagger-ui/**,/swagger-ui.html,/v3/api-docs,/v3/api-docs/**,/api/client/auth/register,/api/client/login/**,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars/swagger-ui/**,/api/webjars/**,/api/actuator/**,/actuator/**,/api/almanac/**,/api/almanac,/api/health
rate-limit:
max-requests: 1000
time-window-seconds: 60
enabled: true
whitelist:
- 127.0.0.1
- localhost
logging:
level:
root: DEBUG
"[io.destiny]": DEBUG
"[org.springdoc]": DEBUG
@@ -0,0 +1,27 @@
server:
port: 8080
spring:
r2dbc:
url: r2dbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
pool:
initial-size: 20
max-size: 50
max-idle-time: 10m
max-life-time: 30m
max-create-connection-time: 5s
webflux:
base-path: /api
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
logging:
level:
root: WARN
"[io.destiny]": INFO
@@ -0,0 +1,62 @@
server:
port: 8080
spring:
cloud:
compatibility-verifier:
enabled: false
r2dbc:
url: r2dbc:h2:mem:///testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
pool:
initial-size: 5
max-size: 20
max-idle-time: 10m
max-life-time: 30m
max-create-connection-time: 5s
flyway:
enabled: true
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
baseline-on-migrate: true
validate-on-migrate: false
clean-disabled: false
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration
- org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration
- org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration
management:
endpoints:
web:
exposure:
include: health,info
base-path: /actuator
endpoint:
health:
show-details: always
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
default-models-expand-depth: 2
show-actuator: false
packages-to-scan:
gateway:
public-paths: /api/client/auth/register,/api/client/auth/login,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/api/swagger-ui.html,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars,/api/actuator/**,/actuator/**,/actuator/health,/actuator/info,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/**,/almanac,/almanac/**
logging:
level:
root: INFO
"[io.destiny]": DEBUG
"[org.springdoc]": DEBUG
@@ -0,0 +1,82 @@
server:
port: 8080
spring:
application:
name: everything-is-suitable-api
main:
allow-bean-definition-overriding: true
profiles:
active: ${SPRING_PROFILES_ACTIVE:local}
gateway:
public-paths: ${GATEWAY_PUBLIC_PATHS:/api/client/auth/register,/api/client/auth/login,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/api/health,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars/swagger-ui/**,/api/webjars/**,/api/actuator/**,/api/almanac,/api/almanac/**,/actuator/**,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/swagger-ui/**,/webjars/**,/almanac,/almanac/**}
springdoc:
api-docs:
enabled: ${SWAGGER_ENABLED:true}
path: /v3/api-docs
resolve-schema-properties: true
swagger-ui:
enabled: ${SWAGGER_ENABLED:true}
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
show-actuator: false
management:
endpoints:
enabled-by-default: true
web:
exposure:
include: "*"
base-path: /actuator
endpoint:
health:
show-details: always
show-components: always
enabled: true
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
step: 30s
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:local}
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5,0.95,0.99
slo:
http.server.requests: 50ms,100ms,200ms,500ms,1s
enable:
jvm: true
process: true
system: true
logback: false
tomcat: false
hibernate: false
cache: false
logging:
level:
root: INFO
"[io.destiny]": DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
max-size: 100MB
max-history: 30
total-size-cap: 1GB
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 30
total-size-cap: 1GB
clean-history-on-start: true
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="everything-is-suitable-api"/>
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="logs"/>
<springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO"/>
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-debug.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-debug.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
</appender>
<logger name="io.destiny" level="${LOG_LEVEL}" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ALL"/>
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="FILE_DEBUG"/>
</logger>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ALL"/>
</logger>
<logger name="org.springframework.web" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ALL"/>
</logger>
<root level="${LOG_LEVEL}">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ALL"/>
<appender-ref ref="FILE_ERROR"/>
</root>
</configuration>
@@ -0,0 +1,63 @@
package io.destiny;
import io.destiny.gateway.config.GatewayConfig;
import io.destiny.gateway.config.GatewayProperties;
import io.destiny.statistics.config.StatisticsConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = Application.class)
@ActiveProfiles("test")
@Import(TestConfig.class)
class ApplicationIntegrationTest {
@Autowired
private ApplicationContext applicationContext;
@Test
void contextLoads() {
assertThat(applicationContext).isNotNull();
}
@Test
void gatewayConfigLoaded() {
assertThat(applicationContext.getBeansOfType(GatewayConfig.class)).hasSize(1);
}
@Test
void gatewayPropertiesLoaded() {
assertThat(applicationContext.getBeansOfType(GatewayProperties.class)).hasSize(1);
}
@Test
void statisticsConfigurationLoaded() {
assertThat(applicationContext.getBeansOfType(StatisticsConfiguration.class)).hasSize(1);
}
@Test
void allRoutersLoaded() {
assertThat(applicationContext.getBean("clientRouter")).isNotNull();
assertThat(applicationContext.getBean("healthRouter")).isNotNull();
assertThat(applicationContext.getBean("ziweiRoutes")).isNotNull();
assertThat(applicationContext.getBean("almanacRoutes")).isNotNull();
}
@Test
void allHandlersLoaded() {
assertThat(applicationContext.getBean("almanacHandler")).isNotNull();
assertThat(applicationContext.getBean("clientHandler")).isNotNull();
assertThat(applicationContext.getBean("healthHandler")).isNotNull();
assertThat(applicationContext.getBean("ziweiHandler")).isNotNull();
}
@Test
void exceptionHandlersLoaded() {
assertThat(applicationContext.getBean("reactiveGlobalExceptionHandler")).isNotNull();
}
}
@@ -0,0 +1,17 @@
package io.destiny;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("应用程序测试")
class ApplicationTest {
@Test
@DisplayName("应该成功创建应用程序")
void testApplicationCreation() {
Application app = new Application();
assertNotNull(app, "应用程序不应为null");
}
}
@@ -0,0 +1,54 @@
package io.destiny;
import io.destiny.biz.service.ICalendarService;
import io.destiny.biz.service.ILunarCalendarService;
import io.destiny.biz.service.IZiweiChartService;
import io.destiny.sys.core.service.ISysPermissionInitService;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.r2dbc.core.DatabaseClient;
import static org.mockito.Mockito.mock;
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public DatabaseClient databaseClient(ConnectionFactory connectionFactory) {
return DatabaseClient.create(connectionFactory);
}
@Bean
@Primary
public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) {
return new R2dbcEntityTemplate(connectionFactory);
}
@Bean
@Primary
public ILunarCalendarService lunarCalendarService() {
return mock(ILunarCalendarService.class);
}
@Bean
@Primary
public ICalendarService calendarService(ILunarCalendarService lunarCalendarService) {
return mock(ICalendarService.class);
}
@Bean
@Primary
public IZiweiChartService ziweiChartService() {
return mock(IZiweiChartService.class);
}
@Bean
@Primary
public ISysPermissionInitService sysPermissionInitService() {
return mock(ISysPermissionInitService.class);
}
}
@@ -0,0 +1,319 @@
package io.destiny.api.performance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@DisplayName("API综合性能测试")
class ApiPerformanceTest {
private WebTestClient webTestClient;
@BeforeEach
void setUp() {
webTestClient = WebTestClient.bindToServer()
.baseUrl("http://localhost:8080")
.build();
warmUpConnection();
}
private void warmUpConnection() {
try {
for (int i = 0; i < 3; i++) {
webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
}
} catch (Exception e) {
System.err.println("预热连接失败: " + e.getMessage());
}
}
@Test
@DisplayName("登录接口性能测试 - 应该在500ms内完成")
void testLoginPerformance() {
long startTime = System.currentTimeMillis();
var response = webTestClient.post()
.uri("/api/sys/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"username\":\"admin\",\"password\":\"admin123\"}")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
String responseString = response != null ? new String(response) : null;
System.out.println("登录接口响应时间: " + responseTime + "ms");
System.out.println("响应内容: " + responseString);
assertNotNull(response, "登录响应不应为空");
assertTrue(responseTime < 1500,
String.format("登录接口响应时间应该在1500ms内,实际: %dms", responseTime));
if (responseString != null && responseString.contains("\"token\"")) {
System.out.println("获取到认证令牌");
}
}
@Test
@DisplayName("健康检查接口性能测试 - 应该在200ms内完成")
void testHealthCheckPerformance() {
long startTime = System.currentTimeMillis();
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
System.out.println("健康检查接口响应时间: " + responseTime + "ms");
System.out.println("响应内容: " + response);
assertNotNull(response, "健康检查响应不应为空");
assertTrue(responseTime < 200,
String.format("健康检查接口响应时间应该在200ms内,实际: %dms", responseTime));
}
@Test
@DisplayName("并发登录请求测试 - 10个并发登录请求应该在5000ms内完成")
void testConcurrentLoginRequests() throws InterruptedException {
int concurrentRequests = 10;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
byte[] responseBytes = webTestClient.post()
.uri("/api/sys/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"username\":\"admin\",\"password\":\"admin123\"}")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
String response = responseBytes != null ? new String(responseBytes) : null;
if (response != null && response.contains("\"token\"")) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("登录请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("并发登录请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
assertTrue(successCount.get() > 0, "至少应该有一个登录请求成功");
assertTrue(totalTime < 5000,
String.format("%d个并发登录请求应该在5000ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("连续请求稳定性测试 - 50次连续请求不应该出现性能下降")
void testContinuousRequestStability() {
List<Long> responseTimes = new ArrayList<>();
int requestCount = 50;
for (int i = 0; i < requestCount; i++) {
long startTime = System.currentTimeMillis();
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
responseTimes.add(responseTime);
assertNotNull(response, "健康检查响应不应为空");
if (i % 10 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(50);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
long avgResponseTime = responseTimes.stream().mapToLong(Long::longValue).sum() / requestCount;
long maxResponseTime = responseTimes.stream().mapToLong(Long::longValue).max().orElse(0L);
long minResponseTime = responseTimes.stream().mapToLong(Long::longValue).min().orElse(0L);
System.out.println("=== 连续请求稳定性测试结果 ===");
System.out.println("总请求数: " + requestCount);
System.out.println("平均响应时间: " + avgResponseTime + "ms");
System.out.println("最大响应时间: " + maxResponseTime + "ms");
System.out.println("最小响应时间: " + minResponseTime + "ms");
System.out.println("响应时间波动: " + (maxResponseTime - minResponseTime) + "ms");
assertTrue(avgResponseTime < 200,
String.format("平均响应时间应该在200ms内,实际: %dms", avgResponseTime));
assertTrue(maxResponseTime < 500,
String.format("最大响应时间应该在500ms内,实际: %dms", maxResponseTime));
}
@Test
@DisplayName("高并发压力测试 - 100个并发请求应该能够正常处理")
void testHighConcurrencyStress() throws InterruptedException {
int concurrentRequests = 100;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
if (response != null) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(15, TimeUnit.SECONDS);
System.out.println("=== 高并发压力测试结果 ===");
System.out.println("并发请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
System.out.println("成功率: " + (successCount.get() * 100.0 / concurrentRequests) + "%");
assertTrue(successCount.get() > concurrentRequests * 0.70,
String.format("成功率应该大于70%%,实际: %.2f%%", successCount.get() * 100.0 / concurrentRequests));
assertTrue(totalTime < 20000,
String.format("%d个高并发请求应该在20000ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("内存使用测试 - 连续100次请求不应该有明显的内存泄漏")
void testMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
runtime.gc();
long initialMemory = runtime.totalMemory() - runtime.freeMemory();
for (int i = 0; i < 100; i++) {
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
assertNotNull(response, "健康检查响应不应为空");
if (i % 20 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(10);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
runtime.gc();
long finalMemory = runtime.totalMemory() - runtime.freeMemory();
long memoryIncrease = finalMemory - initialMemory;
System.out.println("初始内存使用: " + (initialMemory / 1024 / 1024) + " MB");
System.out.println("最终内存使用: " + (finalMemory / 1024 / 1024) + " MB");
System.out.println("内存增长: " + (memoryIncrease / 1024 / 1024) + " MB");
assertTrue(memoryIncrease < 20 * 1024 * 1024,
String.format("100次请求后内存增长应该小于20MB,实际: %.2fMB",
memoryIncrease / 1024.0 / 1024.0));
}
private static void assertNotNull(Object obj, String message) {
if (obj == null) {
throw new AssertionError(message);
}
}
private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
}
@@ -0,0 +1,314 @@
package io.destiny.api.performance;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("API实际性能测试")
class RealApiPerformanceTest {
private HttpClient httpClient;
@BeforeEach
void setUp() {
httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
warmUpConnection();
}
private void warmUpConnection() {
try {
for (int i = 0; i < 3; i++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
} catch (Exception e) {
System.err.println("预热连接失败: " + e.getMessage());
}
}
@Test
@DisplayName("健康检查接口性能测试 - 应该在100ms内完成")
void testHealthCheckPerformance() throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
System.out.println("健康检查接口响应时间: " + responseTime + "ms");
System.out.println("响应状态: " + response.statusCode());
assertEquals(200, response.statusCode(), "健康检查接口应该返回200状态");
assertTrue(responseTime < 100,
String.format("健康检查接口响应时间应该在100ms内,实际: %dms", responseTime));
}
@Test
@DisplayName("登录接口性能测试 - 应该在500ms内完成")
void testLoginPerformance() throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/sys/auth/login"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"username\":\"admin\",\"password\":\"admin123\"}"))
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
System.out.println("登录接口响应时间: " + responseTime + "ms");
System.out.println("响应状态: " + response.statusCode());
assertEquals(200, response.statusCode(), "登录接口应该返回200状态");
assertTrue(responseTime < 500,
String.format("登录接口响应时间应该在500ms内,实际: %dms", responseTime));
}
@Test
@DisplayName("并发健康检查测试 - 10个并发请求应该在500ms内完成")
void testConcurrentHealthCheckRequests() throws InterruptedException {
int concurrentRequests = 10;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("健康检查请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("并发健康检查请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
assertEquals(concurrentRequests, successCount.get(),
"所有并发健康检查请求都应该成功");
assertTrue(totalTime < 500,
String.format("%d个并发健康检查请求应该在500ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("高并发压力测试 - 100个并发请求应该能够正常处理")
void testHighConcurrencyStress() throws InterruptedException {
int concurrentRequests = 100;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(15, TimeUnit.SECONDS);
System.out.println("=== 高并发压力测试结果 ===");
System.out.println("并发请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
System.out.println("成功率: " + (successCount.get() * 100.0 / concurrentRequests) + "%");
assertTrue(successCount.get() > concurrentRequests * 0.95,
String.format("成功率应该大于95%%,实际: %.2f%%", successCount.get() * 100.0 / concurrentRequests));
assertTrue(totalTime < 5000,
String.format("%d个高并发请求应该在5000ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("连续请求稳定性测试 - 50次连续请求不应该出现性能下降")
void testContinuousRequestStability() throws InterruptedException {
List<Long> responseTimes = new ArrayList<>();
int requestCount = 50;
for (int i = 0; i < requestCount; i++) {
long startTime = System.currentTimeMillis();
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
responseTimes.add(responseTime);
assertEquals(200, response.statusCode(), "健康检查接口应该返回200状态");
if (i % 10 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(50);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
long avgResponseTime = responseTimes.stream().mapToLong(Long::longValue).sum() / requestCount;
long maxResponseTime = responseTimes.stream().mapToLong(Long::longValue).max().orElse(0L);
long minResponseTime = responseTimes.stream().mapToLong(Long::longValue).min().orElse(0L);
System.out.println("=== 连续请求稳定性测试结果 ===");
System.out.println("总请求数: " + requestCount);
System.out.println("平均响应时间: " + avgResponseTime + "ms");
System.out.println("最大响应时间: " + maxResponseTime + "ms");
System.out.println("最小响应时间: " + minResponseTime + "ms");
System.out.println("响应时间波动: " + (maxResponseTime - minResponseTime) + "ms");
assertTrue(avgResponseTime < 100,
String.format("平均响应时间应该在100ms内,实际: %dms", avgResponseTime));
assertTrue(maxResponseTime < 200,
String.format("最大响应时间应该在200ms内,实际: %dms", maxResponseTime));
}
@Test
@DisplayName("内存使用测试 - 连续100次请求不应该有明显的内存泄漏")
void testMemoryUsage() throws InterruptedException {
Runtime runtime = Runtime.getRuntime();
runtime.gc();
long initialMemory = runtime.totalMemory() - runtime.freeMemory();
for (int i = 0; i < 100; i++) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/health"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode(), "健康检查接口应该返回200状态");
if (i % 20 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(10);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
runtime.gc();
long finalMemory = runtime.totalMemory() - runtime.freeMemory();
long memoryIncrease = finalMemory - initialMemory;
System.out.println("初始内存使用: " + (initialMemory / 1024 / 1024) + " MB");
System.out.println("最终内存使用: " + (finalMemory / 1024 / 1024) + " MB");
System.out.println("内存增长: " + (memoryIncrease / 1024 / 1024) + " MB");
assertTrue(memoryIncrease < 20 * 1024 * 1024,
String.format("100次请求后内存增长应该小于20MB,实际: %.2fMB",
memoryIncrease / 1024.0 / 1024.0));
}
private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
private static void assertEquals(long expected, long actual, String message) {
if (expected != actual) {
throw new AssertionError(String.format("%s (expected: %d, actual: %d)", message, expected, actual));
}
}
}
@@ -0,0 +1,265 @@
package io.destiny.api.performance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@DisplayName("API性能测试")
class SimpleApiPerformanceTest {
private WebTestClient webTestClient;
@BeforeEach
void setUp() {
webTestClient = WebTestClient.bindToServer()
.baseUrl("http://localhost:8080")
.build();
}
@Test
@DisplayName("健康检查接口性能测试 - 应该在100ms内完成")
void testHealthCheckPerformance() {
long startTime = System.currentTimeMillis();
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
System.out.println("健康检查接口响应时间: " + responseTime + "ms");
System.out.println("响应状态: " + response.getStatus());
assertTrue(response.getStatus().is2xxSuccessful(),
"健康检查接口应该返回成功状态");
assertTrue(responseTime < 100,
String.format("健康检查接口响应时间应该在100ms内,实际: %dms", responseTime));
}
@Test
@DisplayName("并发健康检查测试 - 10个并发请求应该在500ms内完成")
void testConcurrentHealthCheckRequests() throws InterruptedException {
int concurrentRequests = 10;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
if (response.getStatus().is2xxSuccessful()) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("健康检查请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("并发健康检查请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
assertEquals(concurrentRequests, successCount.get(),
"所有并发健康检查请求都应该成功");
assertTrue(totalTime < 500,
String.format("%d个并发健康检查请求应该在500ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("高并发压力测试 - 100个并发请求应该能够正常处理")
void testHighConcurrencyStress() throws InterruptedException {
int concurrentRequests = 100;
ExecutorService executorService = Executors.newFixedThreadPool(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
if (response.getStatus().is2xxSuccessful()) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
} catch (Exception e) {
failureCount.incrementAndGet();
System.err.println("请求失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
executorService.shutdown();
executorService.awaitTermination(15, TimeUnit.SECONDS);
System.out.println("=== 高并发压力测试结果 ===");
System.out.println("并发请求数: " + concurrentRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + failureCount.get());
System.out.println("总耗时: " + totalTime + "ms");
System.out.println("平均响应时间: " + (totalTime / concurrentRequests) + "ms");
System.out.println("吞吐量: " + (concurrentRequests * 1000.0 / totalTime) + " 请求/秒");
System.out.println("成功率: " + (successCount.get() * 100.0 / concurrentRequests) + "%");
assertTrue(successCount.get() > concurrentRequests * 0.95,
String.format("成功率应该大于95%%,实际: %.2f%%", successCount.get() * 100.0 / concurrentRequests));
assertTrue(totalTime < 5000,
String.format("%d个高并发请求应该在5000ms内完成,实际: %dms", concurrentRequests, totalTime));
}
@Test
@DisplayName("连续请求稳定性测试 - 50次连续请求不应该出现性能下降")
void testContinuousRequestStability() {
List<Long> responseTimes = new ArrayList<>();
int requestCount = 50;
for (int i = 0; i < requestCount; i++) {
long startTime = System.currentTimeMillis();
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
responseTimes.add(responseTime);
assertTrue(response.getStatus().is2xxSuccessful(), "健康检查接口应该返回成功状态");
if (i % 10 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(50);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
long avgResponseTime = responseTimes.stream().mapToLong(Long::longValue).sum() / requestCount;
long maxResponseTime = responseTimes.stream().mapToLong(Long::longValue).max().orElse(0L);
long minResponseTime = responseTimes.stream().mapToLong(Long::longValue).min().orElse(0L);
System.out.println("=== 连续请求稳定性测试结果 ===");
System.out.println("总请求数: " + requestCount);
System.out.println("平均响应时间: " + avgResponseTime + "ms");
System.out.println("最大响应时间: " + maxResponseTime + "ms");
System.out.println("最小响应时间: " + minResponseTime + "ms");
System.out.println("响应时间波动: " + (maxResponseTime - minResponseTime) + "ms");
assertTrue(avgResponseTime < 100,
String.format("平均响应时间应该在100ms内,实际: %dms", avgResponseTime));
assertTrue(maxResponseTime < 200,
String.format("最大响应时间应该在200ms内,实际: %dms", maxResponseTime));
}
@Test
@DisplayName("内存使用测试 - 连续100次请求不应该有明显的内存泄漏")
void testMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
runtime.gc();
long initialMemory = runtime.totalMemory() - runtime.freeMemory();
for (int i = 0; i < 100; i++) {
try {
var response = webTestClient.get()
.uri("/api/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody()
.returnResult();
assertTrue(response.getStatus().is2xxSuccessful(), "健康检查接口应该返回成功状态");
if (i % 20 == 0) {
System.out.println("已完成 " + i + " 次请求");
}
Thread.sleep(10);
} catch (Exception e) {
System.err.println("请求失败: " + e.getMessage());
}
}
runtime.gc();
long finalMemory = runtime.totalMemory() - runtime.freeMemory();
long memoryIncrease = finalMemory - initialMemory;
System.out.println("初始内存使用: " + (initialMemory / 1024 / 1024) + " MB");
System.out.println("最终内存使用: " + (finalMemory / 1024 / 1024) + " MB");
System.out.println("内存增长: " + (memoryIncrease / 1024 / 1024) + " MB");
assertTrue(memoryIncrease < 20 * 1024 * 1024,
String.format("100次请求后内存增长应该小于20MB,实际: %.2fMB",
memoryIncrease / 1024.0 / 1024.0));
}
private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
private static void assertEquals(long expected, long actual, String message) {
if (expected != actual) {
throw new AssertionError(String.format("%s (expected: %d, actual: %d)", message, expected, actual));
}
}
}
@@ -0,0 +1,80 @@
package io.destiny.api.test;
import io.qameta.allure.Allure;
import io.qameta.allure.Step;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.test.context.ActiveProfiles;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Import(io.destiny.TestConfig.class)
public abstract class BaseApiTest {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
protected DatabaseClient databaseClient;
@Autowired
protected R2dbcEntityTemplate r2dbcEntityTemplate;
@BeforeEach
public void setUp() {
log.info("Setting up test: {}", this.getClass().getSimpleName());
Allure.step("Test setup completed");
}
@AfterEach
public void tearDown() {
log.info("Tearing down test: {}", this.getClass().getSimpleName());
Allure.step("Test teardown completed");
}
@Step("Execute SQL: {sql}")
protected Mono<Void> executeSql(String sql) {
return databaseClient.sql(sql)
.then()
.doOnSuccess(v -> log.debug("SQL executed successfully"))
.doOnError(e -> log.error("SQL execution failed", e));
}
@Step("Clean table: {tableName}")
protected Mono<Void> cleanTable(String tableName) {
return executeSql("DELETE FROM " + tableName)
.doOnSuccess(v -> log.debug("Table {} cleaned", tableName));
}
@Step("Insert test data into {tableName}")
protected Mono<Void> insertTestData(String tableName, String columns, String values) {
String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columns, values);
return executeSql(sql);
}
@Step("Count records in table: {tableName}")
protected Mono<Long> countRecords(String tableName) {
return databaseClient.sql("SELECT COUNT(*) FROM " + tableName)
.map(row -> row.get(0, Long.class))
.first();
}
protected void verifyMono(Mono<?> mono) {
StepVerifier.create(mono)
.verifyComplete();
}
protected <T> void verifyMonoWithValue(Mono<T> mono, T expected) {
StepVerifier.create(mono)
.expectNext(expected)
.verifyComplete();
}
}
@@ -0,0 +1,173 @@
package io.destiny.api.test.service;
import io.destiny.api.test.BaseApiTest;
import io.destiny.sys.core.domain.SysMenu;
import io.destiny.sys.core.service.ISysMenuService;
import io.qameta.allure.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
@Epic("菜单管理")
@Feature("菜单服务")
class SysMenuServiceTest extends BaseApiTest {
@Autowired
private ISysMenuService menuService;
@BeforeEach
void setUpData() {
cleanTable("sys_menu").block();
}
@Test
@Story("创建菜单")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("创建菜单 - 成功")
void testCreateMenu_Success() {
Allure.step("Given: 准备菜单数据");
SysMenu menu = createTestMenu("系统管理", 0L, 1, "M", null, null);
Allure.step("When: 创建菜单");
StepVerifier.create(menuService.create(menu))
.assertNext(savedMenu -> {
Allure.step("Then: 验证菜单创建成功");
assertNotNull(savedMenu.getId(), "菜单ID应自动生成");
assertEquals("系统管理", savedMenu.getMenuName(), "菜单名称应匹配");
assertEquals(0L, savedMenu.getParentId(), "父级ID应匹配");
assertEquals("M", savedMenu.getMenuType(), "菜单类型应匹配");
assertNotNull(savedMenu.getCreatedAt(), "创建时间应设置");
assertNotNull(savedMenu.getUpdatedAt(), "更新时间应设置");
})
.verifyComplete();
}
@Test
@Story("创建菜单")
@Severity(SeverityLevel.NORMAL)
@DisplayName("创建子菜单 - 成功")
void testCreateSubMenu_Success() {
Allure.step("Given: 创建父菜单");
SysMenu parentMenu = createTestMenu("系统管理", 0L, 1, "M", null, null);
SysMenu savedParent = menuService.create(parentMenu).block();
assertNotNull(savedParent, "父菜单应创建成功");
Allure.step("And: 准备子菜单数据");
SysMenu subMenu = createTestMenu("用户管理", savedParent.getId(), 1, "C", "system:user:list", "system/user/index");
Allure.step("When: 创建子菜单");
StepVerifier.create(menuService.create(subMenu))
.assertNext(savedSubMenu -> {
Allure.step("Then: 验证子菜单创建成功");
assertNotNull(savedSubMenu.getId(), "子菜单ID应自动生成");
assertEquals("用户管理", savedSubMenu.getMenuName(), "菜单名称应匹配");
assertEquals(savedParent.getId(), savedSubMenu.getParentId(), "父级ID应匹配");
assertEquals("C", savedSubMenu.getMenuType(), "菜单类型应为菜单");
assertEquals("system:user:list", savedSubMenu.getPerms(), "权限标识应匹配");
})
.verifyComplete();
}
@Test
@Story("查询菜单")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("根据ID查询菜单 - 成功")
void testFindById_Success() {
Allure.step("Given: 创建测试菜单");
SysMenu menu = createTestMenu("查询菜单", 0L, 1, "M", null, null);
SysMenu savedMenu = menuService.create(menu).block();
assertNotNull(savedMenu, "菜单应创建成功");
Long menuId = savedMenu.getId();
Allure.step("When: 根据ID查询菜单");
StepVerifier.create(menuService.findById(menuId))
.assertNext(foundMenu -> {
Allure.step("Then: 验证菜单信息正确");
assertNotNull(foundMenu, "查询到的菜单不应为null");
assertEquals("查询菜单", foundMenu.getMenuName(), "菜单名称应匹配");
})
.verifyComplete();
}
@Test
@Story("更新菜单")
@Severity(SeverityLevel.NORMAL)
@DisplayName("更新菜单 - 成功")
void testUpdateMenu_Success() {
Allure.step("Given: 创建测试菜单");
SysMenu menu = createTestMenu("更新前菜单", 0L, 1, "M", null, null);
SysMenu savedMenu = menuService.create(menu).block();
assertNotNull(savedMenu, "菜单应创建成功");
Allure.step("When: 更新菜单信息");
savedMenu.setMenuName("更新后菜单");
savedMenu.setOrderNum(2);
StepVerifier.create(menuService.update(savedMenu))
.assertNext(updatedMenu -> {
Allure.step("Then: 验证菜单更新成功");
assertEquals("更新后菜单", updatedMenu.getMenuName(), "菜单名称应已更新");
assertEquals(2, updatedMenu.getOrderNum(), "排序应已更新");
assertNotNull(updatedMenu.getUpdatedAt(), "更新时间应设置");
})
.verifyComplete();
}
@Test
@Story("删除菜单")
@Severity(SeverityLevel.NORMAL)
@DisplayName("删除菜单 - 成功(软删除)")
void testDeleteMenu_Success() {
Allure.step("Given: 创建测试菜单");
SysMenu menu = createTestMenu("删除菜单", 0L, 1, "M", null, null);
SysMenu savedMenu = menuService.create(menu).block();
assertNotNull(savedMenu, "菜单应创建成功");
Long menuId = savedMenu.getId();
Allure.step("When: 删除菜单(软删除)");
StepVerifier.create(menuService.deleteById(menuId))
.verifyComplete();
Allure.step("Then: 验证菜单已软删除(deletedAt 已设置)");
StepVerifier.create(menuService.findById(menuId))
.assertNext(deletedMenu -> {
assertNotNull(deletedMenu.getDeletedAt(), "删除时间应已设置");
})
.verifyComplete();
}
@Test
@Story("查询所有菜单")
@Severity(SeverityLevel.NORMAL)
@DisplayName("查询所有菜单 - 成功")
void testFindAll_Success() {
Allure.step("Given: 创建多个测试菜单");
SysMenu menu1 = createTestMenu("菜单1", 0L, 1, "M", null, null);
SysMenu menu2 = createTestMenu("菜单2", 0L, 2, "M", null, null);
menuService.create(menu1).block();
menuService.create(menu2).block();
Allure.step("When: 查询所有菜单");
StepVerifier.create(menuService.findAll())
.expectNextCount(2)
.verifyComplete();
}
private SysMenu createTestMenu(String menuName, Long parentId, Integer orderNum, String menuType, String perms, String component) {
SysMenu menu = new SysMenu();
menu.setMenuName(menuName);
menu.setParentId(parentId);
menu.setOrderNum(orderNum);
menu.setMenuType(menuType);
menu.setPerms(perms);
menu.setComponent(component);
menu.setStatus("0");
menu.setCreatedAt(LocalDateTime.now());
menu.setUpdatedAt(LocalDateTime.now());
return menu;
}
}
@@ -0,0 +1,164 @@
package io.destiny.api.test.service;
import io.destiny.api.test.BaseApiTest;
import io.destiny.sys.core.domain.SysRole;
import io.destiny.sys.core.service.ISysRoleService;
import io.qameta.allure.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
@Epic("角色管理")
@Feature("角色服务")
class SysRoleServiceTest extends BaseApiTest {
@Autowired
private ISysRoleService roleService;
@BeforeEach
void setUpData() {
cleanTable("sys_role").block();
}
@Test
@Story("创建角色")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("创建角色 - 成功")
void testCreateRole_Success() {
Allure.step("Given: 准备角色数据");
SysRole role = createTestRole("测试角色", "test_role", 1);
Allure.step("When: 创建角色");
StepVerifier.create(roleService.create(role))
.assertNext(savedRole -> {
Allure.step("Then: 验证角色创建成功");
assertNotNull(savedRole.getId(), "角色ID应自动生成");
assertEquals("测试角色", savedRole.getRoleName(), "角色名称应匹配");
assertEquals("test_role", savedRole.getRoleKey(), "角色键应匹配");
assertEquals(1, savedRole.getRoleSort(), "排序应匹配");
assertNotNull(savedRole.getCreatedAt(), "创建时间应设置");
assertNotNull(savedRole.getUpdatedAt(), "更新时间应设置");
})
.verifyComplete();
}
@Test
@Story("查询角色")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("根据ID查询角色 - 成功")
void testFindById_Success() {
Allure.step("Given: 创建测试角色");
SysRole role = createTestRole("查询角色", "query_role", 1);
SysRole savedRole = roleService.create(role).block();
assertNotNull(savedRole, "角色应创建成功");
Long roleId = savedRole.getId();
Allure.step("When: 根据ID查询角色");
StepVerifier.create(roleService.findById(roleId))
.assertNext(foundRole -> {
Allure.step("Then: 验证角色信息正确");
assertNotNull(foundRole, "查询到的角色不应为null");
assertEquals("查询角色", foundRole.getRoleName(), "角色名称应匹配");
assertEquals("query_role", foundRole.getRoleKey(), "角色键应匹配");
})
.verifyComplete();
}
@Test
@Story("查询角色")
@Severity(SeverityLevel.NORMAL)
@DisplayName("根据角色键查询角色 - 成功")
void testFindByRoleKey_Success() {
Allure.step("Given: 创建测试角色");
SysRole role = createTestRole("角色键测试", "key_test_role", 2);
roleService.create(role).block();
Allure.step("When: 根据角色键查询");
StepVerifier.create(roleService.findByRoleKey("key_test_role"))
.assertNext(foundRole -> {
Allure.step("Then: 验证角色信息正确");
assertEquals("角色键测试", foundRole.getRoleName(), "角色名称应匹配");
assertEquals("key_test_role", foundRole.getRoleKey(), "角色键应匹配");
})
.verifyComplete();
}
@Test
@Story("更新角色")
@Severity(SeverityLevel.NORMAL)
@DisplayName("更新角色 - 成功")
void testUpdateRole_Success() {
Allure.step("Given: 创建测试角色");
SysRole role = createTestRole("更新前角色", "before_update", 1);
SysRole savedRole = roleService.create(role).block();
assertNotNull(savedRole, "角色应创建成功");
Allure.step("When: 更新角色信息");
savedRole.setRoleName("更新后角色");
savedRole.setRoleSort(2);
StepVerifier.create(roleService.update(savedRole))
.assertNext(updatedRole -> {
Allure.step("Then: 验证角色更新成功");
assertEquals("更新后角色", updatedRole.getRoleName(), "角色名称应已更新");
assertEquals(2, updatedRole.getRoleSort(), "排序应已更新");
assertNotNull(updatedRole.getUpdatedAt(), "更新时间应设置");
})
.verifyComplete();
}
@Test
@Story("删除角色")
@Severity(SeverityLevel.NORMAL)
@DisplayName("删除角色 - 成功(软删除)")
void testDeleteRole_Success() {
Allure.step("Given: 创建测试角色");
SysRole role = createTestRole("删除角色", "delete_role", 1);
SysRole savedRole = roleService.create(role).block();
assertNotNull(savedRole, "角色应创建成功");
Long roleId = savedRole.getId();
Allure.step("When: 删除角色(软删除)");
StepVerifier.create(roleService.deleteById(roleId))
.verifyComplete();
Allure.step("Then: 验证角色已软删除(deletedAt 已设置)");
StepVerifier.create(roleService.findById(roleId))
.assertNext(deletedRole -> {
assertNotNull(deletedRole.getDeletedAt(), "删除时间应已设置");
})
.verifyComplete();
}
@Test
@Story("查询所有角色")
@Severity(SeverityLevel.NORMAL)
@DisplayName("查询所有角色 - 成功")
void testFindAll_Success() {
Allure.step("Given: 创建多个测试角色");
SysRole role1 = createTestRole("角色1", "role_1", 1);
SysRole role2 = createTestRole("角色2", "role_2", 2);
roleService.create(role1).block();
roleService.create(role2).block();
Allure.step("When: 查询所有角色");
StepVerifier.create(roleService.findAll())
.expectNextCount(2)
.verifyComplete();
}
private SysRole createTestRole(String roleName, String roleKey, Integer roleSort) {
SysRole role = new SysRole();
role.setRoleName(roleName);
role.setRoleKey(roleKey);
role.setRoleSort(roleSort);
role.setStatus("0");
role.setCreatedAt(LocalDateTime.now());
role.setUpdatedAt(LocalDateTime.now());
return role;
}
}
@@ -0,0 +1,191 @@
package io.destiny.api.test.service;
import io.destiny.api.test.BaseApiTest;
import io.destiny.sys.core.domain.SysUser;
import io.destiny.sys.core.service.ISysUserService;
import io.destiny.sys.core.service.ISysPermissionInitService;
import io.qameta.allure.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
@Epic("用户管理")
@Feature("用户服务")
class SysUserServiceTest extends BaseApiTest {
@Autowired
private ISysUserService userService;
@MockitoBean
private ISysPermissionInitService permissionInitService;
@BeforeEach
void setUpData() {
cleanTable("sys_user").block();
when(permissionInitService.initializeUserPermissions(anyLong())).thenReturn(Mono.empty());
when(permissionInitService.initializeAdminPermissions(anyLong())).thenReturn(Mono.empty());
}
@Test
@Story("查询用户")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("根据ID查询用户 - 成功")
void testFindById_Success() {
Allure.step("Given: 创建测试用户");
SysUser user = createTestUser("testuser", "test@example.com");
SysUser savedUser = userService.create(user).block();
assertNotNull(savedUser, "用户应创建成功");
Long userId = savedUser.getId();
Allure.step("When: 根据ID查询用户");
StepVerifier.create(userService.findById(userId))
.assertNext(foundUser -> {
Allure.step("Then: 验证用户信息正确");
assertNotNull(foundUser, "查询到的用户不应为null");
assertEquals("testuser", foundUser.getUsername(), "用户名应匹配");
assertEquals("test@example.com", foundUser.getEmail().getValue(), "邮箱应匹配");
})
.verifyComplete();
}
@Test
@Story("查询用户")
@Severity(SeverityLevel.NORMAL)
@DisplayName("根据ID查询用户 - 用户不存在")
void testFindById_NotFound() {
Allure.step("Given: 不存在的用户ID");
Long nonExistentId = 999999L;
Allure.step("When: 查询不存在的用户");
StepVerifier.create(userService.findById(nonExistentId))
.verifyComplete();
}
@Test
@Story("查询用户")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("根据用户名查询用户 - 成功")
void testFindByUsername_Success() {
Allure.step("Given: 创建测试用户");
SysUser user = createTestUser("findbyusername", "findbyusername@example.com");
userService.create(user).block();
Allure.step("When: 根据用户名查询用户");
StepVerifier.create(userService.findByUsername("findbyusername"))
.assertNext(foundUser -> {
Allure.step("Then: 验证用户信息正确");
assertNotNull(foundUser, "查询到的用户不应为null");
assertEquals("findbyusername", foundUser.getUsername(), "用户名应匹配");
})
.verifyComplete();
}
@Test
@Story("创建用户")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("创建新用户 - 成功")
void testCreateUser_Success() {
Allure.step("Given: 准备用户数据");
SysUser user = createTestUser("newuser", "newuser@example.com");
Allure.step("When: 创建用户");
StepVerifier.create(userService.create(user))
.assertNext(createdUser -> {
Allure.step("Then: 验证用户创建成功");
assertNotNull(createdUser.getId(), "用户ID应自动生成");
assertEquals("newuser", createdUser.getUsername(), "用户名应匹配");
assertEquals("newuser@example.com", createdUser.getEmail().getValue(), "邮箱应匹配");
assertEquals("0", createdUser.getStatus(), "状态应为正常");
assertNotNull(createdUser.getCreatedAt(), "创建时间应设置");
})
.verifyComplete();
}
@Test
@Story("更新用户")
@Severity(SeverityLevel.NORMAL)
@DisplayName("更新用户信息 - 成功")
void testUpdateUser_Success() {
Allure.step("Given: 创建测试用户");
SysUser user = createTestUser("updateuser", "updateuser@example.com");
SysUser savedUser = userService.create(user).block();
assertNotNull(savedUser, "用户应创建成功");
Allure.step("When: 更新用户信息");
savedUser.setStatus("1");
savedUser.setUpdatedAt(LocalDateTime.now());
StepVerifier.create(userService.update(savedUser))
.assertNext(updatedUser -> {
Allure.step("Then: 验证用户更新成功");
assertEquals("1", updatedUser.getStatus(), "状态应更新为禁用");
assertNotNull(updatedUser.getUpdatedAt(), "更新时间应设置");
})
.verifyComplete();
}
@Test
@Story("删除用户")
@Severity(SeverityLevel.NORMAL)
@DisplayName("删除用户 - 成功(软删除)")
void testDeleteUser_Success() {
Allure.step("Given: 创建测试用户");
SysUser user = createTestUser("deleteuser", "deleteuser@example.com");
SysUser savedUser = userService.create(user).block();
assertNotNull(savedUser, "用户应创建成功");
Long userId = savedUser.getId();
Allure.step("When: 删除用户(软删除)");
StepVerifier.create(userService.deleteById(userId))
.verifyComplete();
Allure.step("Then: 验证用户已软删除(deletedAt 已设置)");
StepVerifier.create(userService.findById(userId))
.assertNext(deletedUser -> {
assertNotNull(deletedUser.getDeletedAt(), "删除时间应已设置");
})
.verifyComplete();
}
@Test
@Story("查询所有用户")
@Severity(SeverityLevel.NORMAL)
@DisplayName("查询所有用户 - 成功")
void testFindAll_Success() {
Allure.step("Given: 创建多个测试用户");
SysUser user1 = createTestUser("user1", "user1@example.com");
SysUser user2 = createTestUser("user2", "user2@example.com");
userService.create(user1).block();
userService.create(user2).block();
Allure.step("When: 查询所有用户");
StepVerifier.create(userService.findAll().collectList())
.assertNext(users -> {
Allure.step("Then: 验证用户列表");
assertNotNull(users, "用户列表不应为null");
assertTrue(users.size() >= 2, "应至少有2个用户");
})
.verifyComplete();
}
private SysUser createTestUser(String username, String email) {
SysUser user = new SysUser();
user.generateId();
user.setUsername(username);
user.setPassword("password123");
user.setEmail(io.destiny.common.primitive.EmailAddress.of(email));
user.setPhone(io.destiny.common.primitive.PhoneNumber.of("13800138000"));
user.setStatus("0");
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return user;
}
}
@@ -0,0 +1,68 @@
package io.destiny.config;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Configuration
@Profile("test")
public class R2dbcInitConfig {
private static final Logger logger = LoggerFactory.getLogger(R2dbcInitConfig.class);
@Bean
@Order(1)
public CommandLineRunner initDatabase(ConnectionFactory connectionFactory) {
return args -> {
logger.info("=== Starting R2DBC database initialization ===");
try {
ClassPathResource resource = new ClassPathResource("schema.sql");
if (!resource.exists()) {
logger.error("schema.sql not found in classpath");
throw new RuntimeException("schema.sql not found in classpath");
}
String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
logger.info("Loaded schema.sql, size: {} bytes", sql.length());
Connection connection = Mono.from(connectionFactory.create()).block();
if (connection == null) {
throw new RuntimeException("Failed to create database connection");
}
try {
String[] statements = sql.split(";");
logger.info("Found {} SQL statements to execute", statements.length);
for (int i = 0; i < statements.length; i++) {
String trimmed = statements[i].trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("--")) {
logger.info("Executing statement {}/{}: {}", i + 1, statements.length,
trimmed.length() > 100 ? trimmed.substring(0, 100) + "..." : trimmed);
Mono.from(connection.createStatement(trimmed).execute()).block();
}
}
logger.info("=== R2DBC database initialized successfully ===");
} finally {
Mono.from(connection.close()).block();
}
} catch (Exception e) {
logger.error("=== Failed to initialize R2DBC database ===", e);
throw new RuntimeException("Failed to initialize R2DBC database", e);
}
};
}
}
@@ -0,0 +1,79 @@
package io.destiny.config;
import io.destiny.sys.core.domain.SysUser;
import io.destiny.sys.core.repository.ISysUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ActiveProfiles("test")
class TestDataConfigTest {
@Autowired
private ISysUserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.findAll()
.flatMap(user -> userRepository.deleteById(user.getId()))
.blockLast();
}
@Test
void shouldCreateAndFindAdminUser() {
SysUser adminUser = new SysUser();
adminUser.setUsername("admin");
adminUser.setPassword("password123");
adminUser.setStatus("0");
adminUser.setCreatedAt(LocalDateTime.now());
adminUser.setUpdatedAt(LocalDateTime.now());
StepVerifier.create(userRepository.save(adminUser))
.assertNext(savedUser -> {
assertThat(savedUser).isNotNull();
assertThat(savedUser.getUsername()).isEqualTo("admin");
assertThat(savedUser.getStatus()).isEqualTo("0");
})
.verifyComplete();
StepVerifier.create(userRepository.findByUsername("admin"))
.assertNext(user -> {
assertThat(user).isNotNull();
assertThat(user.getUsername()).isEqualTo("admin");
})
.verifyComplete();
}
@Test
void shouldCreateAndFindTestUser() {
SysUser testUser = new SysUser();
testUser.setUsername("test_user");
testUser.setPassword("password123");
testUser.setStatus("0");
testUser.setCreatedAt(LocalDateTime.now());
testUser.setUpdatedAt(LocalDateTime.now());
StepVerifier.create(userRepository.save(testUser))
.assertNext(savedUser -> {
assertThat(savedUser).isNotNull();
assertThat(savedUser.getUsername()).isEqualTo("test_user");
assertThat(savedUser.getStatus()).isEqualTo("0");
})
.verifyComplete();
StepVerifier.create(userRepository.findByUsername("test_user"))
.assertNext(user -> {
assertThat(user).isNotNull();
assertThat(user.getUsername()).isEqualTo("test_user");
})
.verifyComplete();
}
}
@@ -0,0 +1,197 @@
package io.destiny.gateway.config;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("CORS配置测试")
class CorsConfigTest {
@Test
@DisplayName("应该成功创建CORS配置")
void testCorsConfigCreation() {
CorsConfig corsConfig = new CorsConfig();
assertNotNull(corsConfig, "CORS配置不应为null");
}
@Test
@DisplayName("应该成功创建CORS Web过滤器")
void testCorsWebFilterCreation() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
}
@Test
@DisplayName("应该正确配置CORS源")
void testCorsConfigurationSource() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
Object configSource = configSourceField.get(filter);
assertNotNull(configSource, "配置源不应为null");
assertTrue(configSource instanceof UrlBasedCorsConfigurationSource,
"配置源应为UrlBasedCorsConfigurationSource类型");
} catch (Exception e) {
fail("无法访问配置源字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该允许所有来源")
void testCorsAllowsAllOrigins() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
UrlBasedCorsConfigurationSource configSource = (UrlBasedCorsConfigurationSource) configSourceField
.get(filter);
Field corsConfigurationsField = UrlBasedCorsConfigurationSource.class
.getDeclaredField("corsConfigurations");
corsConfigurationsField.setAccessible(true);
Object corsConfigurations = corsConfigurationsField.get(configSource);
assertNotNull(corsConfigurations, "CORS配置不应为null");
} catch (Exception e) {
fail("无法访问CORS配置字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该允许所有HTTP方法")
void testCorsAllowsAllMethods() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
UrlBasedCorsConfigurationSource configSource = (UrlBasedCorsConfigurationSource) configSourceField
.get(filter);
Field corsConfigurationsField = UrlBasedCorsConfigurationSource.class
.getDeclaredField("corsConfigurations");
corsConfigurationsField.setAccessible(true);
Object corsConfigurations = corsConfigurationsField.get(configSource);
assertNotNull(corsConfigurations, "CORS配置不应为null");
} catch (Exception e) {
fail("无法访问CORS配置字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该允许所有请求头")
void testCorsAllowsAllHeaders() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
UrlBasedCorsConfigurationSource configSource = (UrlBasedCorsConfigurationSource) configSourceField
.get(filter);
Field corsConfigurationsField = UrlBasedCorsConfigurationSource.class
.getDeclaredField("corsConfigurations");
corsConfigurationsField.setAccessible(true);
Object corsConfigurations = corsConfigurationsField.get(configSource);
assertNotNull(corsConfigurations, "CORS配置不应为null");
} catch (Exception e) {
fail("无法访问CORS配置字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该允许凭证")
void testCorsAllowsCredentials() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
UrlBasedCorsConfigurationSource configSource = (UrlBasedCorsConfigurationSource) configSourceField
.get(filter);
Field corsConfigurationsField = UrlBasedCorsConfigurationSource.class
.getDeclaredField("corsConfigurations");
corsConfigurationsField.setAccessible(true);
Object corsConfigurations = corsConfigurationsField.get(configSource);
assertNotNull(corsConfigurations, "CORS配置不应为null");
} catch (Exception e) {
fail("无法访问CORS配置字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该允许预检请求缓存")
void testCorsAllowsPreflightCache() {
CorsConfig corsConfig = new CorsConfig();
CorsWebFilter filter = corsConfig.corsWebFilter();
assertNotNull(filter, "CORS Web过滤器不应为null");
try {
Field configSourceField = CorsWebFilter.class.getDeclaredField("configSource");
configSourceField.setAccessible(true);
UrlBasedCorsConfigurationSource configSource = (UrlBasedCorsConfigurationSource) configSourceField
.get(filter);
Field corsConfigurationsField = UrlBasedCorsConfigurationSource.class
.getDeclaredField("corsConfigurations");
corsConfigurationsField.setAccessible(true);
Object corsConfigurations = corsConfigurationsField.get(configSource);
assertNotNull(corsConfigurations, "CORS配置不应为null");
} catch (Exception e) {
fail("无法访问CORS配置字段: " + e.getMessage());
}
}
@Test
@DisplayName("应该支持多个CORS配置实例")
void testMultipleCorsConfigInstances() {
CorsConfig corsConfig1 = new CorsConfig();
CorsConfig corsConfig2 = new CorsConfig();
CorsWebFilter filter1 = corsConfig1.corsWebFilter();
CorsWebFilter filter2 = corsConfig2.corsWebFilter();
assertNotNull(filter1, "第一个CORS Web过滤器不应为null");
assertNotNull(filter2, "第二个CORS Web过滤器不应为null");
assertNotSame(filter1, filter2, "两个CORS Web过滤器应该是不同的实例");
}
}
@@ -0,0 +1,111 @@
package io.destiny.health;
import org.junit.jupiter.api.DisplayName;
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.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.FetchSpec;
import reactor.core.publisher.Mono;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("数据库健康指示器测试")
class DatabaseHealthIndicatorTest {
@Mock
private DatabaseClient databaseClient;
@Mock
private DatabaseClient.GenericExecuteSpec executeSpec;
@Mock
private FetchSpec<Map<String, Object>> fetchSpec;
@Test
@DisplayName("应该成功创建健康指示器")
void testDatabaseHealthIndicatorCreation() {
DatabaseHealthIndicator indicator = new DatabaseHealthIndicator(databaseClient);
assertNotNull(indicator, "健康指示器不应为null");
}
@Test
@DisplayName("当数据库连接成功时应该返回UP状态")
void testHealthReturnsUpWhenDatabaseIsConnected() {
when(databaseClient.sql(anyString())).thenReturn(executeSpec);
when(executeSpec.fetch()).thenReturn(fetchSpec);
when(fetchSpec.one()).thenReturn(Mono.just(Map.of()));
DatabaseHealthIndicator indicator = new DatabaseHealthIndicator(databaseClient);
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertEquals(Status.UP, health.getStatus(), "状态应该是UP");
assertEquals("PostgreSQL", health.getDetails().get("database"), "数据库类型应该是PostgreSQL");
assertEquals("Connected", health.getDetails().get("status"), "状态应该是Connected");
}
@Test
@DisplayName("当数据库连接失败时应该返回DOWN状态")
void testHealthReturnsDownWhenDatabaseIsDisconnected() {
when(databaseClient.sql(anyString())).thenReturn(executeSpec);
when(executeSpec.fetch()).thenReturn(fetchSpec);
when(fetchSpec.one()).thenReturn(Mono.error(new RuntimeException("Connection failed")));
DatabaseHealthIndicator indicator = new DatabaseHealthIndicator(databaseClient);
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertEquals(Status.DOWN, health.getStatus(), "状态应该是DOWN");
assertEquals("PostgreSQL", health.getDetails().get("database"), "数据库类型应该是PostgreSQL");
assertNotNull(health.getDetails().get("status"), "状态不应为null");
assertTrue(health.getDetails().containsKey("reason"), "应该包含reason详情");
}
@Test
@DisplayName("当数据库查询抛出异常时应该返回DOWN状态")
void testHealthReturnsDownWhenDatabaseThrowsException() {
when(databaseClient.sql(anyString())).thenReturn(executeSpec);
when(executeSpec.fetch()).thenReturn(fetchSpec);
when(fetchSpec.one()).thenReturn(Mono.error(new RuntimeException("Database error")));
DatabaseHealthIndicator indicator = new DatabaseHealthIndicator(databaseClient);
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertEquals(Status.DOWN, health.getStatus(), "状态应该是DOWN");
assertEquals("PostgreSQL", health.getDetails().get("database"), "数据库类型应该是PostgreSQL");
assertNotNull(health.getDetails().get("status"), "状态不应为null");
assertNotNull(health.getDetails().get("reason"), "应该包含reason详情");
}
@Test
@DisplayName("应该支持多次调用health方法")
void testHealthMultipleCalls() {
when(databaseClient.sql(anyString())).thenReturn(executeSpec);
when(executeSpec.fetch()).thenReturn(fetchSpec);
when(fetchSpec.one()).thenReturn(Mono.just(Map.of()));
DatabaseHealthIndicator indicator = new DatabaseHealthIndicator(databaseClient);
Health health1 = indicator.health();
Health health2 = indicator.health();
assertNotNull(health1, "第一次调用返回的健康状态不应为null");
assertNotNull(health2, "第二次调用返回的健康状态不应为null");
}
}
@@ -0,0 +1,129 @@
package io.destiny.health;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("磁盘空间健康指示器测试")
class DiskSpaceHealthIndicatorTest {
@Test
@DisplayName("应该成功创建健康指示器")
void testDiskSpaceHealthIndicatorCreation() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
assertNotNull(indicator, "健康指示器不应为null");
}
@Test
@DisplayName("应该返回健康状态")
void testHealthReturnsHealth() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertTrue(health.getDetails().containsKey("freeSpace"), "应该包含freeSpace详情");
assertTrue(health.getDetails().containsKey("totalSpace"), "应该包含totalSpace详情");
assertTrue(health.getDetails().containsKey("usableSpace"), "应该包含usableSpace详情");
assertTrue(health.getDetails().containsKey("freeSpacePercent"), "应该包含freeSpacePercent详情");
}
@Test
@DisplayName("应该返回包含磁盘空间详情的健康状态")
void testHealthReturnsDiskSpaceDetails() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
assertFalse(health.getDetails().isEmpty(), "健康详情不应为空");
}
@Test
@DisplayName("应该正确格式化字节大小")
void testFormatBytes() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
String freeSpace = (String) health.getDetails().get("freeSpace");
assertNotNull(freeSpace, "freeSpace不应为null");
assertTrue(freeSpace.matches("^[0-9.]+\\s+[BKBMG]{1,2}$"), "freeSpace格式应该正确");
}
@Test
@DisplayName("应该返回有效的磁盘空间百分比")
void testFreeSpacePercentIsValid() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
String freeSpacePercent = (String) health.getDetails().get("freeSpacePercent");
assertNotNull(freeSpacePercent, "freeSpacePercent不应为null");
assertTrue(freeSpacePercent.matches("^[0-9.]+%$"), "freeSpacePercent格式应该正确");
}
@Test
@DisplayName("应该支持多次调用health方法")
void testHealthMultipleCalls() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health1 = indicator.health();
Health health2 = indicator.health();
assertNotNull(health1, "第一次调用返回的健康状态不应为null");
assertNotNull(health2, "第二次调用返回的健康状态不应为null");
assertNotNull(health1.getDetails(), "第一次调用的健康详情不应为null");
assertNotNull(health2.getDetails(), "第二次调用的健康详情不应为null");
}
@Test
@DisplayName("应该返回包含所有必需详情的健康状态")
void testHealthContainsAllRequiredDetails() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertTrue(health.getDetails().containsKey("freeSpace"), "应该包含freeSpace详情");
assertTrue(health.getDetails().containsKey("totalSpace"), "应该包含totalSpace详情");
assertTrue(health.getDetails().containsKey("usableSpace"), "应该包含usableSpace详情");
assertTrue(health.getDetails().containsKey("freeSpacePercent"), "应该包含freeSpacePercent详情");
}
@Test
@DisplayName("当磁盘空间充足时应该返回UP状态")
void testHealthReturnsUpWhenDiskSpaceIsSufficient() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertEquals(Status.UP, health.getStatus(), "状态应该是UP");
}
@Test
@DisplayName("应该正确计算磁盘空间百分比")
void testFreeSpacePercentCalculation() {
DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
String freeSpacePercent = (String) health.getDetails().get("freeSpacePercent");
assertNotNull(freeSpacePercent, "freeSpacePercent不应为null");
String percentValue = freeSpacePercent.replace("%", "");
double percent = Double.parseDouble(percentValue);
assertTrue(percent >= 0 && percent <= 100, "百分比应该在0到100之间");
}
}
@@ -0,0 +1,116 @@
package io.destiny.health;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("系统资源健康指示器测试")
class SystemResourceHealthIndicatorTest {
@Test
@DisplayName("应该成功创建健康指示器")
void testSystemResourceHealthIndicatorCreation() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
assertNotNull(indicator, "健康指示器不应为null");
}
@Test
@DisplayName("应该返回健康状态")
void testHealthReturnsHealth() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
}
@Test
@DisplayName("应该返回包含CPU详情的健康状态")
void testHealthReturnsCpuDetails() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
assertFalse(health.getDetails().isEmpty(), "健康详情不应为空");
}
@Test
@DisplayName("应该返回包含内存详情的健康状态")
void testHealthReturnsMemoryDetails() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
assertFalse(health.getDetails().isEmpty(), "健康详情不应为空");
}
@Test
@DisplayName("应该返回包含系统详情的健康状态")
void testHealthReturnsSystemDetails() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
assertFalse(health.getDetails().isEmpty(), "健康详情不应为空");
}
@Test
@DisplayName("应该返回包含资源使用率详情的健康状态")
void testHealthReturnsResourceUsageDetails() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
assertFalse(health.getDetails().isEmpty(), "健康详情不应为空");
}
@Test
@DisplayName("应该支持多次调用health方法")
void testHealthMultipleCalls() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health1 = indicator.health();
Health health2 = indicator.health();
assertNotNull(health1, "第一次调用返回的健康状态不应为null");
assertNotNull(health2, "第二次调用返回的健康状态不应为null");
assertNotNull(health1.getDetails(), "第一次调用的健康详情不应为null");
assertNotNull(health2.getDetails(), "第二次调用的健康详情不应为null");
}
@Test
@DisplayName("应该返回包含系统信息的健康详情")
void testHealthContainsSystemInfo() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getDetails(), "健康详情不应为null");
}
@Test
@DisplayName("应该返回有效的健康状态")
void testHealthReturnsValidStatus() {
SystemResourceHealthIndicator indicator = new SystemResourceHealthIndicator();
Health health = indicator.health();
assertNotNull(health, "健康状态不应为null");
assertNotNull(health.getStatus(), "健康状态不应为null");
assertTrue(health.getStatus().equals(Status.UP) || health.getStatus().equals(Status.DOWN),
"状态应该是UP或DOWN");
}
}
@@ -0,0 +1,25 @@
spring:
application:
name: everything-is-suitable-api-test
r2dbc:
url: r2dbc:h2:mem:///testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
pool:
enabled: true
initial-size: 2
max-size: 10
flyway:
enabled: false
sql:
init:
mode: always
schema-locations: classpath:schema.sql
gateway:
public-paths: /api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/actuator/**,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/swagger-ui/**,/webjars/**,/almanac,/almanac/**
logging:
level:
root: INFO
"[io.destiny]": DEBUG
@@ -0,0 +1,68 @@
-- 测试环境数据库初始化脚本
-- 创建系统用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
role_name VARCHAR(30) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INT DEFAULT 0,
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建用户角色关联表
CREATE TABLE IF NOT EXISTS sys_user_role (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
UNIQUE (user_id, role_id)
);
-- 创建菜单权限表
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INT DEFAULT 0,
menu_type VARCHAR(1) NOT NULL,
perms VARCHAR(100),
component VARCHAR(200),
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建角色菜单关联表
CREATE TABLE IF NOT EXISTS sys_role_menu (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
UNIQUE (role_id, menu_id)
);