Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Omika.Data.HfSql.Protection;
|
||||
|
||||
namespace Omika.Tests.HfSql;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="HfsqlFilePasswordDeriver"/>.
|
||||
/// All test values are synthetic — no real historical secrets
|
||||
/// or derived production passwords are present.
|
||||
/// </summary>
|
||||
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<ArgumentException>(() => Deriver.DeriveProtectionStd(null!));
|
||||
Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectionStd_EmptyDatabaseName_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => Deriver.DeriveProtectionStd(""));
|
||||
Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectionStd_WhitespaceDatabaseName_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() =>
|
||||
Deriver.DeriveProtectionPlus(null!, FakeSecret));
|
||||
Assert.Contains("Database name", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectionPlus_EmptySecret_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Deriver.DeriveProtectionPlus(TestDbName, ReadOnlySpan<char>.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)
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the internal MD5 computation for test verification only.
|
||||
/// NOT part of the production API.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user