task-193: 历史迁移盘点(docs/migration-inventory.md + 只读审计:整数版本 1..108 连续、无重复、识别 V25_1 历史小版本、命名合规、校验和可复算)+ 8 条测试
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package com.nanri.aiimage.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-193:历史迁移盘点审计(只读)。
|
||||
* 校验 src/main/resources/db 版本化迁移:整数版本 1..max 连续、无重复、命名合规、
|
||||
* 历史小版本(V25_1)被识别、校验和可复算稳定;产出 docs/migration-inventory.md 快照已提交。
|
||||
*/
|
||||
class MigrationInventoryTest {
|
||||
|
||||
private static final Path DB_DIR = Path.of(System.getProperty("user.dir"),
|
||||
"src/main/resources/db");
|
||||
|
||||
private static final Pattern VERSION_FILE = Pattern.compile("^V(\\d+)(?:_(\\d+))?__.*\\.sql$");
|
||||
|
||||
private record Migration(int major, int minor) implements Comparable<Migration> {
|
||||
boolean isInteger() {
|
||||
return minor == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Migration o) {
|
||||
return Integer.compare(major, o.major) != 0
|
||||
? Integer.compare(major, o.major) : Integer.compare(minor, o.minor);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Path> versionFiles() throws IOException {
|
||||
try (Stream<Path> s = Files.list(DB_DIR)) {
|
||||
return s.filter(p -> p.getFileName().toString().startsWith("V"))
|
||||
.filter(p -> VERSION_FILE.matcher(p.getFileName().toString()).matches())
|
||||
.sorted(Comparator.comparing(p -> p.getFileName().toString()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Migration> parseAll(List<Path> files) {
|
||||
List<Migration> list = new ArrayList<>();
|
||||
for (Path f : files) {
|
||||
Matcher m = VERSION_FILE.matcher(f.getFileName().toString());
|
||||
if (m.matches()) {
|
||||
list.add(new Migration(Integer.parseInt(m.group(1)),
|
||||
m.group(2) == null ? 0 : Integer.parseInt(m.group(2))));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Test
|
||||
void integerVersionsSequentialFromOne() throws IOException {
|
||||
List<Migration> all = parseAll(versionFiles());
|
||||
List<Integer> integers = all.stream().filter(Migration::isInteger)
|
||||
.map(Migration::major).sorted().toList();
|
||||
for (int i = 0; i < integers.size(); i++) {
|
||||
assertEquals(i + 1, integers.get(i), "整数版本应 1..N 连续,出现断号");
|
||||
}
|
||||
assertEquals(integers.size(), integers.get(integers.size() - 1), "整数版本应以 max 收尾且覆盖 1..max");
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacySubversionIdentified() throws IOException {
|
||||
List<Migration> all = parseAll(versionFiles());
|
||||
assertTrue(all.stream().anyMatch(m -> m.major() == 25 && m.minor() == 1),
|
||||
"历史小版本 V25_1 应被识别并保留(Flyway 语义 25.1)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDuplicateVersionFiles() throws IOException {
|
||||
List<Path> files = versionFiles();
|
||||
Set<String> names = new HashSet<>();
|
||||
for (Path f : files) {
|
||||
assertTrue(names.add(f.getFileName().toString()), "不应有重复版本文件名: " + f.getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void latestVersionPresentInDirectory() throws IOException {
|
||||
List<Migration> all = parseAll(versionFiles());
|
||||
int maxMajor = all.stream().filter(Migration::isInteger).mapToInt(Migration::major).max().orElse(0);
|
||||
assertTrue(maxMajor >= 108, "最新迁移应 ≥ V108,实际 " + maxMajor);
|
||||
boolean hasLatest = versionFiles().stream()
|
||||
.anyMatch(p -> p.getFileName().toString().startsWith("V" + maxMajor + "__"));
|
||||
assertTrue(hasLatest, "目录应含最新整数版本文件 V" + maxMajor + "__*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrationFileNamesWellFormed() throws IOException {
|
||||
for (Path f : versionFiles()) {
|
||||
String name = f.getFileName().toString();
|
||||
assertTrue(!name.contains(" "), "迁移文件名不应含空格: " + name);
|
||||
assertTrue(name.matches("^V\\d+(?:_\\d+)?__[A-Za-z0-9_.-]+$"),
|
||||
"命名应 V<版本>__<snake 描述>.sql: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void inventoryDocCommittedAndConsistent() throws IOException {
|
||||
Path doc = Path.of(System.getProperty("user.dir"), "docs/migration-inventory.md");
|
||||
assertTrue(Files.exists(doc), "盘点文档应存在并随仓库提交");
|
||||
String text = Files.readString(doc);
|
||||
assertTrue(text.contains("V108"), "文档应记录最新版本");
|
||||
assertTrue(text.contains("25_1") || text.contains("V25_1"), "文档应记录历史小版本");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checksumStableAcrossReads() throws IOException {
|
||||
Map<String, String> first = sha256All(versionFiles());
|
||||
Map<String, String> second = sha256All(versionFiles());
|
||||
assertEquals(first, second, "迁移文件校验和应可复算稳定");
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditRepeatable() throws IOException {
|
||||
// 同一输入两次解析结果一致(纯读,可重复执行)
|
||||
assertEquals(parseAll(versionFiles()), parseAll(versionFiles()));
|
||||
assertEquals(sha256All(versionFiles()).size(), versionFiles().size());
|
||||
}
|
||||
|
||||
private static Map<String, String> sha256All(List<Path> files) throws IOException {
|
||||
Map<String, String> hashes = new HashMap<>();
|
||||
for (Path f : files) {
|
||||
hashes.put(f.getFileName().toString(), sha256(f));
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
private static String sha256(Path f) throws IOException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(Files.readAllBytes(f));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user