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; /** * 网关测试基类 *

* 提供:MockMvc、JWT Token 生成、数据库初始化/清理、Redis Mock *

*/ @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 redisTemplate; @Autowired protected TokenVersionCache tokenVersionCache; /** 测试用 JWT Secret(与 application-test.yml 一致) */ private static final String TEST_JWT_SECRET = "TestSuperSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong!!"; /** * 每个测试类之前重置数据库,确保测试隔离 *

* 重新执行 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_admin", "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(默认 USER 角色,管理员用 generateAdminToken) */ protected String generateAccessToken(Long userId, String phone) { return generateAccessToken(userId, phone, "USER"); } /** * 生成管理员测试 Token(用户 100 + ADMIN 角色) */ protected String generateAdminToken() { return generateAccessToken(100L, "13900000000", "ADMIN"); } /** * 生成测试用 Access Token(含 tokenVersion=0 以兼容单设备登录校验) */ protected String generateAccessToken(Long userId, String phone, String role) { return generateAccessToken(userId, role, 0L); } /** * 生成测试用 Access Token(可指定 tokenVersion,用于单设备登录测试) *

* 同步 DB 的 token_version 到指定值,确保 JwtAuthenticationFilter 的版本校验通过。 */ protected String generateAccessToken(Long userId, String role, Long tokenVersion) { long ver = tokenVersion != null ? tokenVersion : 0L; // 同步 DB 版本:管理员更新 t_admin,普通用户更新 t_user try { if ("ADMIN".equals(role) || "SUPER_ADMIN".equals(role)) { jdbcTemplate.update("UPDATE t_admin SET token_version = ? WHERE id = ?", ver, userId); } 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 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); } } }