| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- using System.Security.Cryptography;
- using System.Text;
- using Admin.NET.Core.Update;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.Migration;
- /// <summary>
- /// 迁移脚本 hash 跨平台行尾兼容契约。
- ///
- /// 核心不变量:
- /// LINE ENDING DIFFERENCE = tolerated
- /// SQL CONTENT DIFFERENCE = blocked
- ///
- /// 即:同一份 SQL 在 Windows(CRLF) 与 Linux(LF) 检出得到的 hash 必须互相认可,
- /// 但只要 SQL 字符内容有任何变化(改 token、增删语句、改空格)就必须判定为不一致。
- /// </summary>
- public class MigrationScriptHashTests
- {
- private const string SqlLf =
- "CREATE TABLE A (\nid BIGINT NOT NULL\n);\n";
- private static byte[] Lf(string s) => Encoding.UTF8.GetBytes(s.Replace("\r\n", "\n"));
- private static byte[] Crlf(string s) => Encoding.UTF8.GetBytes(s.Replace("\r\n", "\n").Replace("\n", "\r\n"));
- private static string RawSha(byte[] b) => Convert.ToHexString(SHA256.HashData(b));
- // TEST 1 — 文件 LF,库里存的是该 LF 文件的原始 hash → 必须通过
- [Fact]
- public void Test1_LfFile_StoredLfRawHash_Matches()
- {
- var file = Lf(SqlLf);
- Assert.True(MigrationScriptHash.Matches(RawSha(file), file));
- }
- // TEST 2 — 文件 LF,库里存的是等价 CRLF 版本的 hash → 必须通过
- [Fact]
- public void Test2_LfFile_StoredCrlfEquivalentHash_Matches()
- {
- var file = Lf(SqlLf);
- var storedFromWindows = RawSha(Crlf(SqlLf));
- Assert.NotEqual(RawSha(file), storedFromWindows); // 前提:两者 hash 确实不同
- Assert.True(MigrationScriptHash.Matches(storedFromWindows, file));
- }
- // TEST 3 — 文件 CRLF,库里存的是等价 LF 版本的 hash → 必须通过
- [Fact]
- public void Test3_CrlfFile_StoredLfEquivalentHash_Matches()
- {
- var file = Crlf(SqlLf);
- var storedFromLinux = RawSha(Lf(SqlLf));
- Assert.NotEqual(RawSha(file), storedFromLinux);
- Assert.True(MigrationScriptHash.Matches(storedFromLinux, file));
- }
- // TEST 4 — 文件 CRLF,库里存的就是该 CRLF 文件的 hash → 必须通过
- [Fact]
- public void Test4_CrlfFile_StoredCrlfHash_Matches()
- {
- var file = Crlf(SqlLf);
- Assert.True(MigrationScriptHash.Matches(RawSha(file), file));
- }
- // TEST 5 — SQL 被真实改写(CREATE TABLE A → B)→ 必须拒绝
- [Fact]
- public void Test5_RealSqlMutation_Rejected()
- {
- var stored = RawSha(Lf(SqlLf));
- var mutated = Lf(SqlLf.Replace("CREATE TABLE A", "CREATE TABLE B"));
- Assert.False(MigrationScriptHash.Matches(stored, mutated));
- }
- // TEST 6 — 增删 SQL token → 必须拒绝
- [Fact]
- public void Test6_TokenAddedOrRemoved_Rejected()
- {
- var stored = RawSha(Lf(SqlLf));
- var added = Lf(SqlLf.Replace("id BIGINT NOT NULL", "id BIGINT NOT NULL, name VARCHAR(10)"));
- Assert.False(MigrationScriptHash.Matches(stored, added));
- var removed = Lf(SqlLf.Replace(" NOT NULL", string.Empty));
- Assert.False(MigrationScriptHash.Matches(stored, removed));
- }
- // TEST 7 — 只改变换行(含孤立 CR 的 Mac 老式行尾)→ 必须通过
- [Fact]
- public void Test7_OnlyLineEndingChanged_Matches()
- {
- var stored = RawSha(Lf(SqlLf));
- Assert.True(MigrationScriptHash.Matches(stored, Crlf(SqlLf)));
- var crOnly = Encoding.UTF8.GetBytes(SqlLf.Replace("\r\n", "\n").Replace("\n", "\r"));
- Assert.True(MigrationScriptHash.Matches(stored, crOnly));
- }
- // TEST 8 — 新 migration 持久化的 hash 必须等于 canonical LF hash,且与平台无关
- [Fact]
- public void Test8_PersistedHashIsCanonicalLf_AndPlatformIndependent()
- {
- var lf = Lf(SqlLf);
- var crlf = Crlf(SqlLf);
- Assert.Equal(RawSha(lf), MigrationScriptHash.ComputeCanonical(lf));
- // 同一份 SQL,无论从哪种行尾的检出执行,写库的 canonical hash 必须一致
- Assert.Equal(MigrationScriptHash.ComputeCanonical(lf), MigrationScriptHash.ComputeCanonical(crlf));
- }
- // 空白差异不属于行尾差异,必须继续拒绝(防止 Matches 被误当成 Trim/去空白)
- [Fact]
- public void WhitespaceOtherThanLineEndings_StillRejected()
- {
- var stored = RawSha(Lf(SqlLf));
- var extraSpace = Lf(SqlLf.Replace("id BIGINT", "id BIGINT"));
- Assert.False(MigrationScriptHash.Matches(stored, extraSpace));
- var trailingSpace = Lf(SqlLf.Replace("CREATE TABLE A (", "CREATE TABLE A ( "));
- Assert.False(MigrationScriptHash.Matches(stored, trailingSpace));
- }
- // 空/缺失 stored hash 不得被当成匹配
- [Fact]
- public void EmptyStoredHash_NeverMatches()
- {
- var file = Lf(SqlLf);
- Assert.False(MigrationScriptHash.Matches(null, file));
- Assert.False(MigrationScriptHash.Matches(string.Empty, file));
- Assert.False(MigrationScriptHash.Matches(" ", file));
- }
- // 大小写不敏感(库里历史记录可能是小写十六进制)
- [Fact]
- public void StoredHashCaseInsensitive()
- {
- var file = Lf(SqlLf);
- Assert.True(MigrationScriptHash.Matches(RawSha(file).ToLowerInvariant(), file));
- }
- }
|