diff --git a/src/Evolve.Cli/Evolve.Cli.csproj b/src/Evolve.Cli/Evolve.Cli.csproj
index 8a1937ed..0c3d7be0 100644
--- a/src/Evolve.Cli/Evolve.Cli.csproj
+++ b/src/Evolve.Cli/Evolve.Cli.csproj
@@ -3,7 +3,7 @@
Exe
- net9.0
+ net10.0
true
EvolveDb.Cli
diff --git a/src/Evolve.Cli/EvolveFactory.cs b/src/Evolve.Cli/EvolveFactory.cs
index e384f288..ad1688e4 100644
--- a/src/Evolve.Cli/EvolveFactory.cs
+++ b/src/Evolve.Cli/EvolveFactory.cs
@@ -45,6 +45,7 @@ public static Evolve Build(Program options, Action logInfoDelegate = nul
RetryRepeatableMigrationsUntilNoError = options.RetryRepeatableMigrationsUntilNoError,
TransactionMode = options.TransactionMode,
SkipNextMigrations = options.SkipNextMigrations,
+ ChecksumAlgorithm = options.ChecksumAlgorithm,
};
if (options.Placeholders != null)
diff --git a/src/Evolve.Cli/Program.cs b/src/Evolve.Cli/Program.cs
index 58df45cc..6715558b 100644
--- a/src/Evolve.Cli/Program.cs
+++ b/src/Evolve.Cli/Program.cs
@@ -6,6 +6,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Security.Cryptography;
using AllowedValuesAttribute = McMaster.Extensions.CommandLineUtils.AllowedValuesAttribute;
[Command(ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated)]
@@ -129,6 +130,13 @@ private int OnExecute(CommandLineApplication app, IConsole console)
[Option("--skip-next-migrations", "When set, mark all subsequent migrations as applied. Default: false", CommandOptionType.SingleValue)]
public bool SkipNextMigrations { get; }
+ [Option("--checksum-algorithm", "Hash algorithm name (e.g., MD5, SHA256, SHA384, SHA512, SHA3-256). Must be supported by the system. Default: MD5", CommandOptionType.SingleValue)]
+ public string ChecksumAlgorithmName { get; } = "MD5";
+
+ public HashAlgorithmName ChecksumAlgorithm => string.IsNullOrWhiteSpace(ChecksumAlgorithmName)
+ ? HashAlgorithmName.MD5
+ : new HashAlgorithmName(ChecksumAlgorithmName);
+
// Cassandra
[Option("--keyspace", "A list of keyspaces managed by Evolve (Cassandra only).", CommandOptionType.MultipleValue)]
public string[] Keyspaces { get; }
diff --git a/src/Evolve.Tool/Evolve.Tool.csproj b/src/Evolve.Tool/Evolve.Tool.csproj
index 9141e82a..257054d2 100644
--- a/src/Evolve.Tool/Evolve.Tool.csproj
+++ b/src/Evolve.Tool/Evolve.Tool.csproj
@@ -3,7 +3,7 @@
Exe
- net9.0
+ net10.0
true
evolve
EvolveDb.Tool
diff --git a/src/Evolve/Configuration/IEvolveConfiguration.cs b/src/Evolve/Configuration/IEvolveConfiguration.cs
index aad44a61..18e0109c 100644
--- a/src/Evolve/Configuration/IEvolveConfiguration.cs
+++ b/src/Evolve/Configuration/IEvolveConfiguration.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Reflection;
+using System.Security.Cryptography;
using System.Text;
using EvolveDb.Migration;
@@ -214,9 +215,23 @@ public interface IEvolveConfiguration
bool SkipNextMigrations { get; }
///
- /// A custom used to load all migrations (applied, pending, ignored...)
+ /// A custom used to load all migrations (applied, pending, ignored...)
/// that replaces the built-in ones ( )
///
IMigrationLoader MigrationLoader { get; }
+
+ ///
+ ///
+ /// Hash algorithm used for calculating migration checksums. (default: MD5)
+ ///
+ ///
+ /// SHA-256, SHA-384, and SHA-512 are FIPS-compliant alternatives.
+ ///
+ ///
+ /// Note: When validating checksums, Evolve will accept both the configured
+ /// algorithm and MD5 for backward compatibility with existing databases.
+ ///
+ ///
+ HashAlgorithmName ChecksumAlgorithm { get; }
}
}
diff --git a/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs b/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs
index 4f3152e3..0e6e2c0e 100644
--- a/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs
+++ b/src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs
@@ -86,7 +86,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
- "checksum VARCHAR(32), " +
+ "checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT now(), " +
"success BOOLEAN NOT NULL " +
diff --git a/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs b/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs
index c1fe244e..4c974d43 100644
--- a/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs
+++ b/src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs
@@ -43,7 +43,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
- "checksum VARCHAR(32), " +
+ "checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"success BOOL NOT NULL " +
diff --git a/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs b/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs
index 635ddcd2..23cadb09 100644
--- a/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs
+++ b/src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs
@@ -44,7 +44,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
- "checksum VARCHAR(32), " +
+ "checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT now(), " +
"success BOOLEAN NOT NULL " +
diff --git a/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs b/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs
index b2fd82f3..3c493471 100644
--- a/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs
+++ b/src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs
@@ -43,7 +43,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
- "checksum VARCHAR(32), " +
+ "checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on DATETIME NOT NULL DEFAULT GETDATE(), " +
"success BIT NOT NULL " +
diff --git a/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs b/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs
index 47ea2b8c..6648ef0f 100644
--- a/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs
+++ b/src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs
@@ -41,7 +41,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
- "checksum VARCHAR(32), " +
+ "checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')), " +
"success BOOLEAN NOT NULL " +
diff --git a/src/Evolve/Evolve.cs b/src/Evolve/Evolve.cs
index 64510adb..68173c1b 100644
--- a/src/Evolve/Evolve.cs
+++ b/src/Evolve/Evolve.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Transactions;
@@ -90,6 +91,8 @@ public IMigrationLoader MigrationLoader
set { _migrationLoader = value; }
}
+ public HashAlgorithmName ChecksumAlgorithm { get; set; } = HashAlgorithmName.MD5;
+
#endregion
#region Properties
diff --git a/src/Evolve/Evolve.csproj b/src/Evolve/Evolve.csproj
index 1fbeeaec..154d9936 100644
--- a/src/Evolve/Evolve.csproj
+++ b/src/Evolve/Evolve.csproj
@@ -2,7 +2,7 @@
- net9.0
+ net10.0
true
true
latest
diff --git a/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs b/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs
index 6e31edb8..4af29677 100644
--- a/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs
+++ b/src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs
@@ -58,7 +58,11 @@ public virtual IEnumerable GetMigrations()
encoding);
})
.ToList()
- .ForEach(x => migrations.Add(x));
+ .ForEach(x =>
+ {
+ x.HashAlgorithm = _options.ChecksumAlgorithm;
+ migrations.Add(x);
+ });
}
return migrations.CheckForDuplicateVersion()
@@ -100,7 +104,11 @@ public virtual IEnumerable GetRepeatableMigrations()
encoding);
})
.ToList()
- .ForEach(x => migrations.Add(x));
+ .ForEach(x =>
+ {
+ x.HashAlgorithm = _options.ChecksumAlgorithm;
+ migrations.Add(x);
+ });
}
return migrations.CheckForDuplicateName()
diff --git a/src/Evolve/Migration/FileMigrationLoader.cs b/src/Evolve/Migration/FileMigrationLoader.cs
index d1b70473..9e6d9360 100644
--- a/src/Evolve/Migration/FileMigrationLoader.cs
+++ b/src/Evolve/Migration/FileMigrationLoader.cs
@@ -59,7 +59,11 @@ public virtual IEnumerable GetMigrations()
return new FileMigrationScript(path: f.FullName, version, description, MetadataType.Migration, encoding);
})
.ToList()
- .ForEach(x => migrations.Add(x));
+ .ForEach(x =>
+ {
+ x.HashAlgorithm = _options.ChecksumAlgorithm;
+ migrations.Add(x);
+ });
}
return migrations.CheckForDuplicateVersion()
@@ -100,7 +104,11 @@ public virtual IEnumerable GetRepeatableMigrations()
return new FileMigrationScript(f.FullName, version: null, description, MetadataType.RepeatableMigration, encoding);
})
.ToList()
- .ForEach(x => migrations.Add(x));
+ .ForEach(x =>
+ {
+ x.HashAlgorithm = _options.ChecksumAlgorithm;
+ migrations.Add(x);
+ });
}
return migrations.CheckForDuplicateName()
diff --git a/src/Evolve/Migration/FileMigrationScript.cs b/src/Evolve/Migration/FileMigrationScript.cs
index ef947d6e..10388c30 100644
--- a/src/Evolve/Migration/FileMigrationScript.cs
+++ b/src/Evolve/Migration/FileMigrationScript.cs
@@ -44,22 +44,32 @@ public override void ValidateChecksum(string? checksum)
}
catch
{
- if (checksum != FallbackCheck())
+ // Try fallback methods for backward compatibility (pre v1.8.0 hashing of file stream)
+ if (checksum == FallbackCheck() ||
+ checksum == FallbackCheckWithAlgorithm(HashAlgorithm))
{
- throw;
+ return;
}
+ throw;
}
}
///
- /// Calculate the checksum with the pre v1.8.0 version.
+ /// Calculate the checksum with the pre v1.8.0 version (reads file directly with MD5).
///
private string FallbackCheck()
{
- using var md5 = MD5.Create();
+ return FallbackCheckWithAlgorithm(HashAlgorithmName.MD5);
+ }
+
+ ///
+ /// Calculate the checksum with the pre v1.8.0 version using specified algorithm.
+ ///
+ /// The hash algorithm to use.
+ private string FallbackCheckWithAlgorithm(HashAlgorithmName algorithm)
+ {
using FileStream stream = File.OpenRead(Path);
- byte[] checksum = md5.ComputeHash(stream);
- return BitConverter.ToString(checksum).Replace("-", string.Empty);
+ return ChecksumCalculator.Calculate(stream, algorithm);
}
}
}
diff --git a/src/Evolve/Migration/MigrationScript.cs b/src/Evolve/Migration/MigrationScript.cs
index efa4ff3f..4a86624d 100644
--- a/src/Evolve/Migration/MigrationScript.cs
+++ b/src/Evolve/Migration/MigrationScript.cs
@@ -25,6 +25,11 @@ protected MigrationScript(string? version, string description, string name, stri
///
public string Content { get; }
+ ///
+ /// Gets or sets the hash algorithm used for checksum calculation.
+ ///
+ internal HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.MD5;
+
///
/// Returns false if the special comment "evolve-tx-off" is found in the first line of the script, true otherwise.
///
@@ -37,7 +42,7 @@ protected MigrationScript(string? version, string description, string name, stri
///
/// Validates the against the actual migration one.
- /// Throws on mismatch.
+ /// Supports dual validation: accepts both the configured algorithm and MD5 for backward compatibility.
///
/// The applied migration checksum.
///
@@ -45,21 +50,56 @@ public virtual void ValidateChecksum(string? checksum)
{
Check.NotNull(checksum, nameof(checksum));
- if (checksum != CalculateChecksum())
+ string currentChecksum = CalculateChecksum();
+
+ // Direct match with configured algorithm
+ if (checksum == currentChecksum)
+ {
+ return;
+ }
+
+ // Dual validation: try MD5 for backward compatibility if using SHA-2
+ if (HashAlgorithm != HashAlgorithmName.MD5)
+ {
+ string md5Checksum = CalculateChecksumWithAlgorithm(HashAlgorithmName.MD5);
+ if (checksum == md5Checksum)
+ {
+ return;
+ }
+ }
+
+ // Detect stored algorithm and recalculate if different
+ var detectedAlgorithm = ChecksumCalculator.DetectAlgorithm(checksum);
+ if (detectedAlgorithm.HasValue && detectedAlgorithm.Value != HashAlgorithm)
{
- throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name));
+ string recalculated = CalculateChecksumWithAlgorithm(detectedAlgorithm.Value);
+ if (checksum == recalculated)
+ {
+ return;
+ }
}
+
+ throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name));
}
///
/// Returns the checksum where crlf and lf line endings have been previously normalized to lf.
///
- ///
+ /// The checksum calculated with the configured hash algorithm.
public virtual string CalculateChecksum()
{
- using var md5 = MD5.Create();
- byte[] checksum = md5.ComputeHash(Encoding.UTF8.GetBytes(NormalizeLineEndings(Content)));
- return BitConverter.ToString(checksum).Replace("-", string.Empty);
+ return CalculateChecksumWithAlgorithm(HashAlgorithm);
+ }
+
+ ///
+ /// Calculates checksum using a specific algorithm.
+ ///
+ /// The hash algorithm to use.
+ /// The checksum calculated with the specified algorithm.
+ protected string CalculateChecksumWithAlgorithm(HashAlgorithmName algorithm)
+ {
+ string normalizedContent = NormalizeLineEndings(Content);
+ return ChecksumCalculator.Calculate(normalizedContent, algorithm);
}
///
diff --git a/src/Evolve/Utilities/ChecksumCalculator.cs b/src/Evolve/Utilities/ChecksumCalculator.cs
new file mode 100644
index 00000000..335df4fa
--- /dev/null
+++ b/src/Evolve/Utilities/ChecksumCalculator.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace EvolveDb.Utilities
+{
+ ///
+ /// Utility class for calculating migration checksums with configurable algorithms.
+ ///
+ internal static class ChecksumCalculator
+ {
+ ///
+ /// Known algorithm lengths for detection.
+ ///
+ private static readonly Dictionary LengthToAlgorithm = new()
+ {
+ { 32, HashAlgorithmName.MD5 },
+ { 64, HashAlgorithmName.SHA256 },
+ { 96, HashAlgorithmName.SHA384 },
+ { 128, HashAlgorithmName.SHA512 }
+ };
+
+ ///
+ /// Returns the expected hex string length for each algorithm.
+ ///
+ /// The hash algorithm name.
+ /// The hex string length (32 for MD5, 64 for SHA256, 96 for SHA384, 128 for SHA512).
+ public static int GetHashLength(HashAlgorithmName algorithm)
+ {
+ if (algorithm == HashAlgorithmName.MD5) return 32;
+ if (algorithm == HashAlgorithmName.SHA256) return 64;
+ if (algorithm == HashAlgorithmName.SHA384) return 96;
+ if (algorithm == HashAlgorithmName.SHA512) return 128;
+ // For unknown algorithms, compute a sample hash to determine length
+ return CryptographicOperations.HashData(algorithm, Array.Empty()).Length * 2;
+ }
+
+ ///
+ /// Detects the hash algorithm based on checksum string length.
+ ///
+ /// The checksum string to analyze.
+ /// The detected algorithm name, or null if length doesn't match any known algorithm.
+ public static HashAlgorithmName? DetectAlgorithm(string? checksum)
+ {
+ if (string.IsNullOrEmpty(checksum))
+ {
+ return null;
+ }
+
+ return LengthToAlgorithm.TryGetValue(checksum.Length, out var algorithm) ? algorithm : null;
+ }
+
+ ///
+ /// Calculates checksum of string content using the specified algorithm.
+ ///
+ /// The string content to hash.
+ /// The hash algorithm to use.
+ /// The uppercase hex string representation of the hash.
+ public static string Calculate(string content, HashAlgorithmName algorithm)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(content);
+ byte[] hash = CryptographicOperations.HashData(algorithm, data);
+ return Convert.ToHexString(hash);
+ }
+
+ ///
+ /// Calculates checksum from a stream using the specified algorithm.
+ ///
+ /// The stream to hash.
+ /// The hash algorithm to use.
+ /// The uppercase hex string representation of the hash.
+ public static string Calculate(Stream stream, HashAlgorithmName algorithm)
+ {
+ byte[] hash = CryptographicOperations.HashData(algorithm, stream);
+ return Convert.ToHexString(hash);
+ }
+ }
+}
diff --git a/test/Evolve.Tests/Evolve.Tests.csproj b/test/Evolve.Tests/Evolve.Tests.csproj
index 59c52dc4..f7cf4600 100644
--- a/test/Evolve.Tests/Evolve.Tests.csproj
+++ b/test/Evolve.Tests/Evolve.Tests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
false
latest
EvolveDb.Tests
diff --git a/test/Evolve.Tests/EvolveConfiguration.cs b/test/Evolve.Tests/EvolveConfiguration.cs
index afcce44f..2cf30da8 100644
--- a/test/Evolve.Tests/EvolveConfiguration.cs
+++ b/test/Evolve.Tests/EvolveConfiguration.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using System.Security.Cryptography;
using System.Text;
using EvolveDb.Configuration;
using EvolveDb.Migration;
@@ -41,6 +42,7 @@ public string MetadataTableSchema
public bool RetryRepeatableMigrationsUntilNoError { get; set; }
public TransactionKind TransactionMode { get; set; } = TransactionKind.CommitEach;
public bool SkipNextMigrations { get; set; } = false;
+ public HashAlgorithmName ChecksumAlgorithm { get; set; } = HashAlgorithmName.MD5;
private IMigrationLoader _migrationLoader;
public IMigrationLoader MigrationLoader
diff --git a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs
index 5890c091..f6e7f38f 100644
--- a/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs
+++ b/test/Evolve.Tests/Migration/FileMigrationScriptTest.cs
@@ -3,6 +3,7 @@
using System.Security.Cryptography;
using EvolveDb.Metadata;
using EvolveDb.Migration;
+using EvolveDb.Utilities;
using Xunit;
using static EvolveDb.Tests.TestContext;
using static EvolveDb.Tests.TestUtil;
@@ -115,5 +116,73 @@ private static string FallbackCheck(string path)
byte[] checksum = md5.ComputeHash(stream);
return BitConverter.ToString(checksum).Replace("-", string.Empty);
}
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void ValidateChecksum_accepts_MD5_when_configured_for_SHA256()
+ {
+ // Arrange
+ var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1");
+ string md5Checksum = script.CalculateChecksum(); // Default is MD5
+
+ // Switch to SHA-256
+ script.HashAlgorithm = HashAlgorithmName.SHA256;
+
+ // Act & Assert - should not throw due to dual validation
+ script.ValidateChecksum(md5Checksum);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void ValidateChecksum_accepts_SHA256_when_configured_for_SHA256()
+ {
+ // Arrange
+ var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1");
+ script.HashAlgorithm = HashAlgorithmName.SHA256;
+ string sha256Checksum = script.CalculateChecksum();
+
+ // Act & Assert - should not throw
+ script.ValidateChecksum(sha256Checksum);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void CalculateChecksum_returns_different_lengths_for_different_algorithms()
+ {
+ // Arrange
+ var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1");
+
+ // Act
+ script.HashAlgorithm = HashAlgorithmName.MD5;
+ string md5 = script.CalculateChecksum();
+
+ script.HashAlgorithm = HashAlgorithmName.SHA256;
+ string sha256 = script.CalculateChecksum();
+
+ script.HashAlgorithm = HashAlgorithmName.SHA512;
+ string sha512 = script.CalculateChecksum();
+
+ // Assert
+ Assert.Equal(32, md5.Length);
+ Assert.Equal(64, sha256.Length);
+ Assert.Equal(128, sha512.Length);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void ValidateChecksum_detects_algorithm_from_stored_checksum_length()
+ {
+ // Arrange - script configured for SHA-512
+ var script = BuildFileMigrationScript(CrLfScriptPath, "2.3.1");
+ script.HashAlgorithm = HashAlgorithmName.SHA512;
+
+ // Calculate SHA-256 checksum separately
+ var tempScript = BuildFileMigrationScript(CrLfScriptPath, "2.3.1");
+ tempScript.HashAlgorithm = HashAlgorithmName.SHA256;
+ string sha256Checksum = tempScript.CalculateChecksum();
+
+ // Act & Assert - should detect SHA-256 from length and validate
+ script.ValidateChecksum(sha256Checksum);
+ }
}
}
diff --git a/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs
new file mode 100644
index 00000000..abb1bdd8
--- /dev/null
+++ b/test/Evolve.Tests/Utilities/ChecksumCalculatorTest.cs
@@ -0,0 +1,127 @@
+using System.Security.Cryptography;
+using EvolveDb.Utilities;
+using Xunit;
+
+namespace EvolveDb.Tests.Utilities
+{
+ public class ChecksumCalculatorTest
+ {
+ private const string TestContent = "SELECT 1;";
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void MD5_returns_32_character_hash()
+ {
+ var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5);
+ Assert.Equal(32, result.Length);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void SHA256_returns_64_character_hash()
+ {
+ var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256);
+ Assert.Equal(64, result.Length);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void SHA384_returns_96_character_hash()
+ {
+ var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA384);
+ Assert.Equal(96, result.Length);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void SHA512_returns_128_character_hash()
+ {
+ var result = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA512);
+ Assert.Equal(128, result.Length);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_correctly_identifies_MD5_by_length()
+ {
+ var checksum = new string('A', 32);
+ Assert.Equal(HashAlgorithmName.MD5, ChecksumCalculator.DetectAlgorithm(checksum));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_correctly_identifies_SHA256_by_length()
+ {
+ var checksum = new string('A', 64);
+ Assert.Equal(HashAlgorithmName.SHA256, ChecksumCalculator.DetectAlgorithm(checksum));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_correctly_identifies_SHA384_by_length()
+ {
+ var checksum = new string('A', 96);
+ Assert.Equal(HashAlgorithmName.SHA384, ChecksumCalculator.DetectAlgorithm(checksum));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_correctly_identifies_SHA512_by_length()
+ {
+ var checksum = new string('A', 128);
+ Assert.Equal(HashAlgorithmName.SHA512, ChecksumCalculator.DetectAlgorithm(checksum));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_returns_null_for_unknown_length()
+ {
+ var checksum = new string('A', 50); // Invalid length
+ Assert.Null(ChecksumCalculator.DetectAlgorithm(checksum));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void DetectAlgorithm_returns_null_for_null_or_empty()
+ {
+ Assert.Null(ChecksumCalculator.DetectAlgorithm(null));
+ Assert.Null(ChecksumCalculator.DetectAlgorithm(""));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void Same_content_produces_same_hash()
+ {
+ var hash1 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256);
+ var hash2 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256);
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void Different_algorithms_produce_different_hashes()
+ {
+ var md5 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5);
+ var sha256 = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.SHA256);
+ Assert.NotEqual(md5, sha256);
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void GetHashLength_returns_correct_values()
+ {
+ Assert.Equal(32, ChecksumCalculator.GetHashLength(HashAlgorithmName.MD5));
+ Assert.Equal(64, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA256));
+ Assert.Equal(96, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA384));
+ Assert.Equal(128, ChecksumCalculator.GetHashLength(HashAlgorithmName.SHA512));
+ }
+
+ [Fact]
+ [Category(Test.Migration)]
+ public void Hash_output_is_uppercase_hex()
+ {
+ var hash = ChecksumCalculator.Calculate(TestContent, HashAlgorithmName.MD5);
+ Assert.Matches("^[0-9A-F]+$", hash);
+ }
+ }
+}