package com.kaidi.finance.setup; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.fasterxml.jackson.databind.JsonNode; import com.kaidi.setup.SetupApplication; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import org.flywaydb.core.Flyway; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @SpringBootTest(classes = SetupApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers(disabledWithoutDocker = true) class SetupApplicationIntegrationTest { private static final String SETUP_CODE = "SETUP-CODE-TEST"; private static final String SETUP_CODE_SHA256 = "9158568c96987884c8141d363daceffda86bb17083054b36105934090aa7e166"; private static final Path STATE_ROOT = Path.of("target", "setup-integration-" + UUID.randomUUID()); @Container static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.4") .withDatabaseName("kaidi_finance") .withUsername("kaidi") .withPassword("setup-database-password"); @DynamicPropertySource static void properties(DynamicPropertyRegistry registry) { registry.add("finance.setup.enabled", () -> true); registry.add("finance.setup.token-sha256", () -> SETUP_CODE_SHA256); registry.add("finance.setup.env-file", () -> STATE_ROOT.resolve("application.env").toString()); registry.add("finance.setup.marker-file", () -> STATE_ROOT.resolve("locked").toString()); registry.add("finance.setup.restart-after-complete", () -> false); registry.add("spring.profiles.active", () -> "setup"); } @Autowired private TestRestTemplate rest; @LocalServerPort private int port; @Test void firstRunTestsDatabaseCreatesAdministratorAndLocksWizard() throws Exception { seedRecoverableV068CollationFailure(); ResponseEntity initial = rest.getForEntity(url("/api/v1/setup/status"), JsonNode.class); assertThat(initial.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(initial.getBody().path("data").path("required").asBoolean()).isTrue(); assertThat(initial.getBody().path("data").path("ready").asBoolean()).isFalse(); Map database = databaseRequest("wrong-code"); ResponseEntity denied = rest.postForEntity(url("/api/v1/setup/test-connection"), new HttpEntity<>(database), JsonNode.class); assertThat(denied.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); assertThat(denied.getBody().path("code").asText()).isEqualTo("SETUP_CODE_INVALID"); database.put("setupCode", SETUP_CODE); ResponseEntity tested = rest.postForEntity(url("/api/v1/setup/test-connection"), new HttpEntity<>(database), JsonNode.class); assertThat(tested.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(tested.getBody().path("data").path("successful").asBoolean()).isTrue(); assertThat(tested.getBody().path("data").path("schemaReady").asBoolean()).isTrue(); assertThat(tested.getBody().path("data").path("message").asText()) .contains("排序规则和完整迁移权限验证通过"); try (Connection connection = DriverManager.getConnection(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()); Statement statement = connection.createStatement()) { try (ResultSet result = statement.executeQuery(""" SELECT (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'kaidi_setup_probe_%') + (SELECT COUNT(*) FROM information_schema.routines WHERE routine_schema = DATABASE() AND routine_name LIKE 'kaidi_setup_probe_%') """)) { result.next(); assertThat(result.getInt(1)).isZero(); } try (ResultSet result = statement.executeQuery(""" SELECT default_collation_name FROM information_schema.schemata WHERE schema_name = DATABASE() """)) { result.next(); assertThat(result.getString(1)).isEqualTo("utf8mb4_0900_ai_ci"); } try (ResultSet result = statement.executeQuery(""" SELECT COUNT(*) FROM flyway_schema_history WHERE success = FALSE """)) { result.next(); assertThat(result.getInt(1)).isZero(); } } Map complete = new LinkedHashMap<>(database); complete.put("adminUsername", "setup-admin"); complete.put("adminDisplayName", "首次安装管理员"); complete.put("adminPassword", "SetupAdmin@2026Strong"); complete.put("adminPasswordConfirmation", "SetupAdmin@2026Strong"); ResponseEntity completed = rest.postForEntity(url("/api/v1/setup/complete"), new HttpEntity<>(complete), JsonNode.class); assertThat(completed.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(completed.getBody().path("data").path("completed").asBoolean()).isTrue(); try (Connection connection = DriverManager.getConnection(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()); Statement statement = connection.createStatement()) { try (ResultSet result = statement.executeQuery(""" SELECT COUNT(*) FROM iam_user user_account JOIN iam_user_role user_role ON user_role.user_id = user_account.id JOIN iam_role role ON role.id = user_role.role_id WHERE user_account.username = 'setup-admin' AND role.code = 'SYSTEM_ADMIN' """)) { result.next(); assertThat(result.getInt(1)).isEqualTo(1); } try (ResultSet result = statement.executeQuery(""" SELECT COUNT(*) FROM iam_scope scope_grant JOIN iam_user user_account ON user_account.id = scope_grant.user_id WHERE user_account.username = 'setup-admin' AND scope_grant.scope_type = 'GLOBAL' """)) { result.next(); assertThat(result.getInt(1)).isGreaterThan(0); } } String environment = Files.readString(STATE_ROOT.resolve("application.env")); assertThat(environment).contains("FINANCE_SETUP_ENABLED=false") .contains("DB_URL=\"jdbc:mysql://") .contains("DB_USERNAME=\"" + MYSQL.getUsername() + "\"") .doesNotContain("SetupAdmin@2026Strong") .doesNotContain(SETUP_CODE); assertThat(Files.exists(STATE_ROOT.resolve("locked"))).isTrue(); ResponseEntity locked = rest.getForEntity(url("/api/v1/setup/status"), JsonNode.class); assertThat(locked.getBody().path("data").path("required").asBoolean()).isFalse(); assertThat(locked.getBody().path("data").path("locked").asBoolean()).isTrue(); assertThat(locked.getBody().path("data").path("ready").asBoolean()).isFalse(); } private void seedRecoverableV068CollationFailure() throws Exception { try (Connection connection = DriverManager.getConnection(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()); Statement statement = connection.createStatement()) { statement.execute("ALTER DATABASE `" + MYSQL.getDatabaseName() + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"); } assertThatThrownBy(() -> Flyway.configure() .dataSource(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()) .locations("classpath:db/migration") .target("68") .load() .migrate()) .hasMessageContaining("V068__source_table_column_validation_metadata.sql"); try (Connection connection = DriverManager.getConnection(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()); Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(""" SELECT COUNT(*) FROM flyway_schema_history WHERE version = '68' AND script = 'V068__source_table_column_validation_metadata.sql' AND success = FALSE """)) { result.next(); assertThat(result.getInt(1)).isEqualTo(1); } } private Map databaseRequest(String setupCode) { Map request = new LinkedHashMap<>(); request.put("setupCode", setupCode); request.put("databaseType", "MYSQL"); request.put("host", MYSQL.getHost()); request.put("port", MYSQL.getFirstMappedPort()); request.put("database", MYSQL.getDatabaseName()); request.put("username", MYSQL.getUsername()); request.put("password", MYSQL.getPassword()); return request; } private String url(String path) { return "http://127.0.0.1:" + port + path; } }