feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+53
@@ -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));
|
||||
}
|
||||
}
|
||||
+47
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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
|
||||
+44
@@ -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
|
||||
+27
@@ -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
|
||||
+62
@@ -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
|
||||
+82
@@ -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
|
||||
+94
@@ -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>
|
||||
+63
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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");
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
+319
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+314
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+265
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -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();
|
||||
}
|
||||
}
|
||||
+173
@@ -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;
|
||||
}
|
||||
}
|
||||
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+191
@@ -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;
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+79
@@ -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();
|
||||
}
|
||||
}
|
||||
+197
@@ -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过滤器应该是不同的实例");
|
||||
}
|
||||
}
|
||||
+111
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+129
@@ -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之间");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+116
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -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)
|
||||
);
|
||||
Reference in New Issue
Block a user