| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package com.xuekairui.gateway;
- import com.xuekairui.user.security.TokenVersionCache;
- import io.jsonwebtoken.Jwts;
- import io.jsonwebtoken.security.Keys;
- import org.junit.jupiter.api.BeforeAll;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.TestInstance;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.context.annotation.Import;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.jdbc.core.JdbcTemplate;
- import org.springframework.jdbc.datasource.init.ScriptUtils;
- import org.springframework.test.context.ActiveProfiles;
- import org.springframework.test.web.servlet.MockMvc;
- import javax.crypto.SecretKey;
- import java.nio.charset.StandardCharsets;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.Map;
- /**
- * 网关测试基类
- * <p>
- * 提供:MockMvc、JWT Token 生成、数据库初始化/清理、Redis Mock
- * </p>
- */
- @SpringBootTest
- @AutoConfigureMockMvc
- @ActiveProfiles("test")
- @Import(TestConfig.class)
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- public abstract class GatewayBaseTest {
- @Autowired
- protected MockMvc mockMvc;
- @Autowired
- protected JdbcTemplate jdbcTemplate;
- @Autowired
- protected RedisTemplate<String, Object> redisTemplate;
- @Autowired
- protected TokenVersionCache tokenVersionCache;
- /** 测试用 JWT Secret(与 application-test.yml 一致) */
- private static final String TEST_JWT_SECRET =
- "TestSuperSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong!!";
- /**
- * 每个测试类之前重置数据库,确保测试隔离
- * <p>
- * 重新执行 test-schema.sql(DROP + CREATE + INSERT),保证每个测试类从干净的数据库状态开始。
- * TRUNCATE 所有业务表以重置 auto_increment 计数器。
- */
- @BeforeAll
- void resetDatabase() {
- try {
- var resource = new ClassPathResource("test-schema.sql");
- // 使用 try-with-resources 确保连接正确释放,避免连接池泄漏
- try (var conn = jdbcTemplate.getDataSource().getConnection()) {
- ScriptUtils.executeSqlScript(conn, resource);
- }
- // TRUNCATE 业务表,重置 auto_increment 计数器
- String[] tables = {
- "t_payment_order_extra_payment", "t_payment_order", "t_payment_plan",
- "t_watchlist", "t_operation_audit_log", "t_trial_quota_config",
- "t_business_license", "t_platform_account", "t_platform_config",
- "t_crawler_quota_grant", "t_crawler_usage_log",
- "t_invite_relation", "t_invite_code", "t_invite_config",
- "t_search_record", "t_platform_session",
- "t_user_coupon", "t_coupon",
- "t_membership_benefit", "t_user_membership", "t_user"
- };
- for (String table : tables) {
- try {
- jdbcTemplate.execute("TRUNCATE TABLE " + table);
- } catch (Exception ignored) { }
- }
- // 重新插入初始数据
- try (var conn = jdbcTemplate.getDataSource().getConnection()) {
- ScriptUtils.executeSqlScript(conn, resource);
- }
- } catch (Exception e) {
- throw new RuntimeException("Failed to reset test database", e);
- }
- }
- /**
- * 每个测试前清理 TokenVersionCache,
- * 确保缓存不会残留其他测试的版本信息。
- * 注意:不重置 DB 的 token_version,避免与 SMS 登录返回的 token 版本冲突。
- * generateAccessToken() 会将 DB 版本同步到 token 版本,保证一致性。
- */
- @BeforeEach
- protected void clearTokenVersionCache() {
- tokenVersionCache.clear();
- }
- /**
- * 生成测试用 Access Token(默认 ADMIN 角色)
- */
- protected String generateAccessToken(Long userId, String phone) {
- return generateAccessToken(userId, phone, "ADMIN");
- }
- /**
- * 生成测试用 Access Token(含 tokenVersion=0 以兼容单设备登录校验)
- */
- protected String generateAccessToken(Long userId, String phone, String role) {
- return generateAccessToken(userId, role, 0L);
- }
- /**
- * 生成测试用 Access Token(可指定 tokenVersion,用于单设备登录测试)
- * <p>
- * 同步 DB 的 token_version 到指定值,确保 JwtAuthenticationFilter 的版本校验通过。
- */
- protected String generateAccessToken(Long userId, String role, Long tokenVersion) {
- long ver = tokenVersion != null ? tokenVersion : 0L;
- // 同步 DB 版本,避免缓存清理后 DB 版本与 token 版本不一致
- try {
- jdbcTemplate.update("UPDATE t_user SET token_version = ? WHERE id = ?", ver, userId);
- } catch (Exception ignored) { }
- SecretKey key = Keys.hmacShaKeyFor(TEST_JWT_SECRET.getBytes(StandardCharsets.UTF_8));
- Map<String, Object> claims = new HashMap<>();
- claims.put("userId", userId);
- claims.put("role", role != null ? role : "USER");
- claims.put("type", "access");
- claims.put("tokenVersion", ver);
- return Jwts.builder()
- .claims(claims)
- .subject(String.valueOf(userId))
- .issuedAt(new Date())
- .expiration(new Date(System.currentTimeMillis() + 86400000))
- .signWith(key)
- .compact();
- }
- /**
- * 初始化数据库(执行建表脚本)
- */
- protected void initDatabase() {
- // 使用 Spring 的 Resource 加载 SQL
- try {
- var resource = new org.springframework.core.io.ClassPathResource("test-schema.sql");
- String sql = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
- jdbcTemplate.execute(sql);
- } catch (Exception e) {
- // 表已存在则忽略
- }
- }
- /**
- * 清理 Redis 限流相关 Key
- */
- protected void clearRateLimitKeys() {
- var keys = redisTemplate.keys("rate:limit:*");
- if (keys != null && !keys.isEmpty()) {
- redisTemplate.delete(keys);
- }
- }
- }
|