GatewayBaseTest.java 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package com.xuekairui.gateway;
  2. import com.xuekairui.user.security.TokenVersionCache;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.security.Keys;
  5. import org.junit.jupiter.api.BeforeAll;
  6. import org.junit.jupiter.api.BeforeEach;
  7. import org.junit.jupiter.api.TestInstance;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  10. import org.springframework.boot.test.context.SpringBootTest;
  11. import org.springframework.context.annotation.Import;
  12. import org.springframework.core.io.ClassPathResource;
  13. import org.springframework.data.redis.core.RedisTemplate;
  14. import org.springframework.jdbc.core.JdbcTemplate;
  15. import org.springframework.jdbc.datasource.init.ScriptUtils;
  16. import org.springframework.test.context.ActiveProfiles;
  17. import org.springframework.test.web.servlet.MockMvc;
  18. import javax.crypto.SecretKey;
  19. import java.nio.charset.StandardCharsets;
  20. import java.util.Date;
  21. import java.util.HashMap;
  22. import java.util.Map;
  23. /**
  24. * 网关测试基类
  25. * <p>
  26. * 提供:MockMvc、JWT Token 生成、数据库初始化/清理、Redis Mock
  27. * </p>
  28. */
  29. @SpringBootTest
  30. @AutoConfigureMockMvc
  31. @ActiveProfiles("test")
  32. @Import(TestConfig.class)
  33. @TestInstance(TestInstance.Lifecycle.PER_CLASS)
  34. public abstract class GatewayBaseTest {
  35. @Autowired
  36. protected MockMvc mockMvc;
  37. @Autowired
  38. protected JdbcTemplate jdbcTemplate;
  39. @Autowired
  40. protected RedisTemplate<String, Object> redisTemplate;
  41. @Autowired
  42. protected TokenVersionCache tokenVersionCache;
  43. /** 测试用 JWT Secret(与 application-test.yml 一致) */
  44. private static final String TEST_JWT_SECRET =
  45. "TestSuperSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong!!";
  46. /**
  47. * 每个测试类之前重置数据库,确保测试隔离
  48. * <p>
  49. * 重新执行 test-schema.sql(DROP + CREATE + INSERT),保证每个测试类从干净的数据库状态开始。
  50. * TRUNCATE 所有业务表以重置 auto_increment 计数器。
  51. */
  52. @BeforeAll
  53. void resetDatabase() {
  54. try {
  55. var resource = new ClassPathResource("test-schema.sql");
  56. // 使用 try-with-resources 确保连接正确释放,避免连接池泄漏
  57. try (var conn = jdbcTemplate.getDataSource().getConnection()) {
  58. ScriptUtils.executeSqlScript(conn, resource);
  59. }
  60. // TRUNCATE 业务表,重置 auto_increment 计数器
  61. String[] tables = {
  62. "t_payment_order_extra_payment", "t_payment_order", "t_payment_plan",
  63. "t_watchlist", "t_operation_audit_log", "t_trial_quota_config",
  64. "t_business_license", "t_platform_account", "t_platform_config",
  65. "t_crawler_quota_grant", "t_crawler_usage_log",
  66. "t_invite_relation", "t_invite_code", "t_invite_config",
  67. "t_search_record", "t_platform_session",
  68. "t_user_coupon", "t_coupon",
  69. "t_membership_benefit", "t_user_membership", "t_user"
  70. };
  71. for (String table : tables) {
  72. try {
  73. jdbcTemplate.execute("TRUNCATE TABLE " + table);
  74. } catch (Exception ignored) { }
  75. }
  76. // 重新插入初始数据
  77. try (var conn = jdbcTemplate.getDataSource().getConnection()) {
  78. ScriptUtils.executeSqlScript(conn, resource);
  79. }
  80. } catch (Exception e) {
  81. throw new RuntimeException("Failed to reset test database", e);
  82. }
  83. }
  84. /**
  85. * 每个测试前清理 TokenVersionCache,
  86. * 确保缓存不会残留其他测试的版本信息。
  87. * 注意:不重置 DB 的 token_version,避免与 SMS 登录返回的 token 版本冲突。
  88. * generateAccessToken() 会将 DB 版本同步到 token 版本,保证一致性。
  89. */
  90. @BeforeEach
  91. protected void clearTokenVersionCache() {
  92. tokenVersionCache.clear();
  93. }
  94. /**
  95. * 生成测试用 Access Token(默认 ADMIN 角色)
  96. */
  97. protected String generateAccessToken(Long userId, String phone) {
  98. return generateAccessToken(userId, phone, "ADMIN");
  99. }
  100. /**
  101. * 生成测试用 Access Token(含 tokenVersion=0 以兼容单设备登录校验)
  102. */
  103. protected String generateAccessToken(Long userId, String phone, String role) {
  104. return generateAccessToken(userId, role, 0L);
  105. }
  106. /**
  107. * 生成测试用 Access Token(可指定 tokenVersion,用于单设备登录测试)
  108. * <p>
  109. * 同步 DB 的 token_version 到指定值,确保 JwtAuthenticationFilter 的版本校验通过。
  110. */
  111. protected String generateAccessToken(Long userId, String role, Long tokenVersion) {
  112. long ver = tokenVersion != null ? tokenVersion : 0L;
  113. // 同步 DB 版本,避免缓存清理后 DB 版本与 token 版本不一致
  114. try {
  115. jdbcTemplate.update("UPDATE t_user SET token_version = ? WHERE id = ?", ver, userId);
  116. } catch (Exception ignored) { }
  117. SecretKey key = Keys.hmacShaKeyFor(TEST_JWT_SECRET.getBytes(StandardCharsets.UTF_8));
  118. Map<String, Object> claims = new HashMap<>();
  119. claims.put("userId", userId);
  120. claims.put("role", role != null ? role : "USER");
  121. claims.put("type", "access");
  122. claims.put("tokenVersion", ver);
  123. return Jwts.builder()
  124. .claims(claims)
  125. .subject(String.valueOf(userId))
  126. .issuedAt(new Date())
  127. .expiration(new Date(System.currentTimeMillis() + 86400000))
  128. .signWith(key)
  129. .compact();
  130. }
  131. /**
  132. * 初始化数据库(执行建表脚本)
  133. */
  134. protected void initDatabase() {
  135. // 使用 Spring 的 Resource 加载 SQL
  136. try {
  137. var resource = new org.springframework.core.io.ClassPathResource("test-schema.sql");
  138. String sql = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
  139. jdbcTemplate.execute(sql);
  140. } catch (Exception e) {
  141. // 表已存在则忽略
  142. }
  143. }
  144. /**
  145. * 清理 Redis 限流相关 Key
  146. */
  147. protected void clearRateLimitKeys() {
  148. var keys = redisTemplate.keys("rate:limit:*");
  149. if (keys != null && !keys.isEmpty()) {
  150. redisTemplate.delete(keys);
  151. }
  152. }
  153. }