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* 重新执行 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,用于单设备登录测试) *
* 同步 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