Files
kaidi/backend/src/test/java/com/kaidi/finance/setup/SetupApplicationIntegrationTest.java
T
Qiufeng 3480d810d4
Release / release (push) Canceled after 0s
fix: recover MySQL collation migration
2026-08-17 22:32:57 +08:00

208 lines
9.9 KiB
Java

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<JsonNode> 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<String, Object> database = databaseRequest("wrong-code");
ResponseEntity<JsonNode> 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<JsonNode> 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<String, Object> complete = new LinkedHashMap<>(database);
complete.put("adminUsername", "setup-admin");
complete.put("adminDisplayName", "首次安装管理员");
complete.put("adminPassword", "SetupAdmin@2026Strong");
complete.put("adminPasswordConfirmation", "SetupAdmin@2026Strong");
ResponseEntity<JsonNode> 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<JsonNode> 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<String, Object> databaseRequest(String setupCode) {
Map<String, Object> 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;
}
}