package com.talentcard.server.business.merchant; import static org.assertj.core.api.Assertions.assertThat; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; /** * 进件新增的表和列在开发库({@code spring.flyway.enabled=false})只能靠 * {@link MerchantOnboardingSchemaInitializer} 补建。 * *

测试库有 Flyway,所以这两个断言在测试里看不出必要性:V144 漏登记时测试全绿,而开发库的 * `GET /api/merchant/onboarding/status` 第一个请求就 500(`Table * 'talent_card.merchant_onboarding_progress' doesn't exist`)。这里查的是"迁移与初始化器是否 * 同步"这件事本身,不依赖数据库。 */ class MerchantOnboardingSchemaInitializerTests { /** V80 建的进件主表:它是加列和后续一切的前提,不属于"迁移后续新增"。 */ private static final Set BASELINE_TABLES = Set.of("merchant_onboarding_application"); private static final Set CONSTRAINT_KEYWORDS = Set.of("primary", "unique", "key", "index", "constraint", "foreign"); @Test void ensuresEveryOnboardingTableAddedByAMigration() throws Exception { Set createdByMigrations = new TreeSet<>(); for (String migration : migrations()) { Matcher matcher = Pattern.compile("(?i)create table (?:if not exists )?(merchant_onboarding_[a-z_]+)").matcher(migration); while (matcher.find()) createdByMigrations.add(matcher.group(1).toLowerCase(Locale.ROOT)); } createdByMigrations.removeAll(BASELINE_TABLES); assertThat(createdByMigrations).as("migrations creating onboarding tables must be discoverable").isNotEmpty(); assertThat(MerchantOnboardingSchemaInitializer.tableDdl().keySet()).containsAll(createdByMigrations); } @Test void initializerTablesCarryTheSameColumnsAsTheMigrationThatCreatedThem() throws Exception { for (var table : MerchantOnboardingSchemaInitializer.tableDdl().entrySet()) { String migrationStatement = createTableStatement(table.getKey()); assertThat(migrationStatement).as("no migration creates %s, so the initializer copy cannot be verified", table.getKey()).isNotNull(); assertThat(columnsOf(table.getValue())).as("columns of %s", table.getKey()).isEqualTo(columnsOf(migrationStatement)); } } /** 初始化器里的建表语句和迁移里必须一致,否则开发库与测试库的表结构会悄悄分叉。 */ private static Set columnsOf(String statement) { List lines = List.of(statement.split("\n")); Set columns = new LinkedHashSet<>(); for (int index = 1; index < lines.size(); index++) { // 第 0 行是 create table 表头 String definition = lines.get(index).strip().replaceFirst(",$", ""); if (definition.isEmpty()) continue; if (definition.startsWith(")")) break; // 列清单结束,后面是表选项 String name = definition.split("\\s+")[0].toLowerCase(Locale.ROOT); if (!CONSTRAINT_KEYWORDS.contains(name)) columns.add(name); } return columns; } private static String createTableStatement(String table) throws Exception { Matcher matcher = Pattern.compile("(?i)create table (?:if not exists )?" + table + "\\s*\\(").matcher(""); for (String migration : migrations()) { matcher.reset(migration); if (matcher.find()) return migration.substring(matcher.start(), migration.indexOf(");", matcher.end())); } return null; } private static List migrations() throws Exception { List statements = new ArrayList<>(); Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:db/migration/*.sql"); for (Resource resource : resources) { try (InputStream stream = resource.getInputStream()) { statements.add(new String(stream.readAllBytes(), StandardCharsets.UTF_8)); } } assertThat(resources).as("migration files must be on the test classpath").isNotEmpty(); return statements; } }