using System.Security.Cryptography; using System.Text; using Omika.Data.HfSql.Protection; namespace Omika.Tests.HfSql; /// /// Unit tests for . /// All test values are synthetic — no real historical secrets /// or derived production passwords are present. /// public sealed class HfsqlFilePasswordDeriverTests { // Synthetic test values — never real production secrets. private const string FakeSecret = "TestSecret_2026"; private const string TestDbName = "MY_TEST_DB"; private static readonly IHfsqlFilePasswordDeriver Deriver = new HfsqlFilePasswordDeriver(); // ================================================================ // ProtectionStd — validation // ================================================================ [Fact] public void ProtectionStd_NullDatabaseName_Throws() { var ex = Assert.Throws(() => Deriver.DeriveProtectionStd(null!)); Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void ProtectionStd_EmptyDatabaseName_Throws() { var ex = Assert.Throws(() => Deriver.DeriveProtectionStd("")); Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void ProtectionStd_WhitespaceDatabaseName_Throws() { var ex = Assert.Throws(() => Deriver.DeriveProtectionStd(" ")); Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase); } // ================================================================ // ProtectionStd — output characteristics // ================================================================ [Fact] public void ProtectionStd_ResultIs32Chars() { var result = Deriver.DeriveProtectionStd(TestDbName); Assert.Equal(32, result.Length); } [Fact] public void ProtectionStd_ResultIsHexOnly() { var result = Deriver.DeriveProtectionStd(TestDbName); Assert.Matches("^[0-9A-F]{32}$", result); } [Fact] public void ProtectionStd_ResultIsDeterministic() { var a = Deriver.DeriveProtectionStd(TestDbName); var b = Deriver.DeriveProtectionStd(TestDbName); Assert.Equal(a, b); } [Fact] public void ProtectionStd_DifferentDatabaseNames_DifferentResults() { var r1 = Deriver.DeriveProtectionStd("DB_ONE"); var r2 = Deriver.DeriveProtectionStd("DB_TWO"); Assert.NotEqual(r1, r2); } [Fact] public void ProtectionStd_CaseIsPreserved() { var upper = Deriver.DeriveProtectionStd("MYDB"); var lower = Deriver.DeriveProtectionStd("mydb"); Assert.NotEqual(upper, lower); } [Fact] public void ProtectionStd_UnderscoreIsPreserved() { var with = Deriver.DeriveProtectionStd("MY_DB"); var without = Deriver.DeriveProtectionStd("MYDB"); Assert.NotEqual(with, without); } [Fact] public void ProtectionStd_NoTrimmingApplied() { var trimmed = Deriver.DeriveProtectionStd("MYDB"); var padded = Deriver.DeriveProtectionStd(" MYDB "); Assert.NotEqual(trimmed, padded); } [Fact] public void ProtectionStd_MatchesManualMd5() { // MD5 of TestDbName using Windows-1252 → hex Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var ansi = Encoding.GetEncoding(1252); byte[] expectedHash = MD5.HashData(ansi.GetBytes(TestDbName)); string expected = Convert.ToHexString(expectedHash); Assert.Equal(expected, Deriver.DeriveProtectionStd(TestDbName)); } // ================================================================ // ProtectionPlus — validation // ================================================================ [Fact] public void ProtectionPlus_NullDatabaseName_Throws() { var ex = Assert.Throws(() => Deriver.DeriveProtectionPlus(null!, FakeSecret)); Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void ProtectionPlus_EmptySecret_Throws() { var ex = Assert.Throws(() => Deriver.DeriveProtectionPlus(TestDbName, ReadOnlySpan.Empty)); Assert.Contains("Historical secret", ex.Message, StringComparison.OrdinalIgnoreCase); } // ================================================================ // ProtectionPlus — output characteristics // ================================================================ [Fact] public void ProtectionPlus_ResultIs32Chars() { var result = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); Assert.Equal(32, result.Length); } [Fact] public void ProtectionPlus_ResultIsHexOnly() { var result = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); Assert.Matches("^[0-9A-F]{32}$", result); } [Fact] public void ProtectionPlus_ResultIsDeterministic() { var a = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); var b = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); Assert.Equal(a, b); } [Fact] public void ProtectionPlus_DiffersFromProtectionStd() { var std = Deriver.DeriveProtectionStd(TestDbName); var plus = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); Assert.NotEqual(std, plus); } [Fact] public void ProtectionPlus_DifferentSecrets_DifferentResults() { var r1 = Deriver.DeriveProtectionPlus(TestDbName, "SecretA"); var r2 = Deriver.DeriveProtectionPlus(TestDbName, "SecretB"); Assert.NotEqual(r1, r2); } // ================================================================ // ProtectionPlus — concatenation ORDER // ================================================================ [Fact] public void ProtectionPlus_SecretFirst_NotDatabaseFirst() { // Historical order: secret + databaseName var actual = Deriver.DeriveProtectionPlus(TestDbName, FakeSecret); // If the order were reversed (databaseName + secret), // the hash would be different. var reversed = Deriver.ComputeMd5ForTest(TestDbName + FakeSecret); Assert.NotEqual(reversed, actual); } [Fact] public void ProtectionPlus_MatchesManualSecretFirstMd5() { // Verify: MD5(FakeSecret + TestDbName) with Windows-1252 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var ansi = Encoding.GetEncoding(1252); byte[] expectedHash = MD5.HashData(ansi.GetBytes(FakeSecret + TestDbName)); string expected = Convert.ToHexString(expectedHash); Assert.Equal(expected, Deriver.DeriveProtectionPlus(TestDbName, FakeSecret)); } // ================================================================ // Encoding: non-ASCII character // ================================================================ [Fact] public void NonAsciiDatabaseName_UsesLegacyAnsiEncoding() { // Database name with a non-ASCII character (É = U+00C9). // In Windows-1252, É is 0xC9 (1 byte). // In UTF-8, É is 0xC3 0x89 (2 bytes). // The two encodings produce different MD5 hashes. const string dbName = "BASE_É_TEST"; var result = Deriver.DeriveProtectionStd(dbName); // Compute the expected value using Windows-1252. Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var ansi = Encoding.GetEncoding(1252); byte[] expectedHash = MD5.HashData(ansi.GetBytes(dbName)); string expectedWindows1252 = Convert.ToHexString(expectedHash); // Compute using UTF-8 for comparison. byte[] utf8Hash = MD5.HashData(Encoding.UTF8.GetBytes(dbName)); string expectedUtf8 = Convert.ToHexString(utf8Hash); Assert.Equal(expectedWindows1252, result); Assert.NotEqual(expectedUtf8, result); // proves encoding matters } } // ================================================================ // Test helper extension (only for verifying concatenation order) // ================================================================ /// /// Exposes the internal MD5 computation for test verification only. /// NOT part of the production API. /// file static class HfsqlFilePasswordDeriverTestHelper { public static string ComputeMd5ForTest(this IHfsqlFilePasswordDeriver _, string input) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var ansi = Encoding.GetEncoding(1252); byte[] hash = MD5.HashData(ansi.GetBytes(input)); return Convert.ToHexString(hash); } }