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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Omika.Core.Domain.Entities.Clients;
|
||||
using Omika.Data.HfSql.Mapping;
|
||||
using Omika.Data.Mapping.Batik;
|
||||
using BatikClientRow = Omika.Data.HfSql.Models.Batik.ClientRow;
|
||||
|
||||
namespace Omika.Tests.HfSql.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Test de bout en bout du pipeline de lecture Client sans base réelle :
|
||||
/// résultat tabulaire simulé (dictionnaire colonne → valeur, comme le
|
||||
/// retournerait <c>IHfsqlConnection.QueryAsync</c>) → <see cref="HfsqlRowMapper"/>
|
||||
/// → <see cref="BatikClientMapper"/> → <see cref="Client"/> canonique.
|
||||
///
|
||||
/// Aucun ODBC, aucun HFSQL, aucune base : uniquement des objets en mémoire.
|
||||
/// </summary>
|
||||
public class HfsqlRowMapperPipelineTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pipeline_SimulatedSqlResult_ToCanonicalClient()
|
||||
{
|
||||
// 1. Résultat tabulaire simulé (ce que QueryAsync retournerait).
|
||||
var sqlRow = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 42L,
|
||||
["CL_Nom"] = "Ville de Lyon",
|
||||
["CL_CodeClient"] = "C001",
|
||||
["CL_CodeTVA"] = "FR123456789",
|
||||
["CL_SIREN"] = "123456789",
|
||||
["CL_SIRET"] = "12345678900012",
|
||||
["CL_Blocage"] = true,
|
||||
["CL_RaisonBlocage"] = "Litige en cours",
|
||||
["Archive"] = false,
|
||||
["CL_Email"] = "contact@lyon.fr",
|
||||
["CL_Tel"] = "0472000000"
|
||||
};
|
||||
|
||||
// 2. Résultat tabulaire → Row physique Batik.
|
||||
BatikClientRow row = HfsqlRowMapper.Map<BatikClientRow>(sqlRow);
|
||||
|
||||
Assert.Equal(42L, row.CLPKID);
|
||||
Assert.Equal("Ville de Lyon", row.CLNom);
|
||||
Assert.Equal("C001", row.CLCodeClient);
|
||||
Assert.True(row.CLBlocage);
|
||||
|
||||
// 3. Row physique → Client canonique (mapper Batik existant).
|
||||
var result = BatikClientMapper.Map(row);
|
||||
|
||||
Client client = result.Value;
|
||||
|
||||
Assert.Equal("C001", client.Code);
|
||||
Assert.Equal("Ville de Lyon", client.Nom);
|
||||
Assert.Equal("FR123456789", client.CodeTva);
|
||||
Assert.Equal("123456789", client.Siren);
|
||||
Assert.Equal("12345678900012", client.Siret);
|
||||
Assert.True(client.EstBloque);
|
||||
Assert.Equal("Litige en cours", client.RaisonBlocage);
|
||||
Assert.False(client.EstArchive);
|
||||
Assert.NotNull(client.Coordonnees);
|
||||
Assert.Equal("contact@lyon.fr", client.Coordonnees!.Email);
|
||||
Assert.Equal("0472000000", client.Coordonnees!.Phone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pipeline_SimulatedSqlResult_WithMissingColumns_StillMaps()
|
||||
{
|
||||
// Un SELECT ciblé (liste) ne ramène que quelques colonnes.
|
||||
var sqlRow = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 7L,
|
||||
["CL_Nom"] = "Mairie de Paris"
|
||||
};
|
||||
|
||||
BatikClientRow row = HfsqlRowMapper.Map<BatikClientRow>(sqlRow);
|
||||
var result = BatikClientMapper.Map(row);
|
||||
|
||||
Client client = result.Value;
|
||||
|
||||
Assert.Equal(7L, row.CLPKID);
|
||||
Assert.Equal("Mairie de Paris", client.Nom);
|
||||
Assert.Null(client.Code);
|
||||
Assert.Null(client.Coordonnees);
|
||||
Assert.False(client.EstBloque);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
using Omika.Data.HfSql.Mapping;
|
||||
using BatikClientRow = Omika.Data.HfSql.Models.Batik.ClientRow;
|
||||
using OmikaClientRow = Omika.Data.HfSql.Models.Omika.ClientRow;
|
||||
using BatikAdresseRow = Omika.Data.HfSql.Models.Batik.AdresseRow;
|
||||
using BatikPersonneRow = Omika.Data.HfSql.Models.Batik.PersonneRow;
|
||||
using BatikLieuRow = Omika.Data.HfSql.Models.Batik.LieuRow;
|
||||
using BatikParametreMetierRow = Omika.Data.HfSql.Models.Batik.ParametreMetierRow;
|
||||
|
||||
namespace Omika.Tests.HfSql.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Tests unitaires de <see cref="HfsqlRowMapper"/> : conversion d'un résultat
|
||||
/// tabulaire physique (dictionnaire colonne → valeur) vers un modèle Row
|
||||
/// physique généré. Aucune base réelle, aucun HFSQL, aucun ODBC.
|
||||
/// </summary>
|
||||
public class HfsqlRowMapperTests
|
||||
{
|
||||
// ---- 1. ClientRow Batik minimal ------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_BatikClientRow_Minimal_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 42L,
|
||||
["CL_Nom"] = "Ville de Lyon",
|
||||
["CL_CodeClient"] = "C001",
|
||||
["CL_Blocage"] = true,
|
||||
["Archive"] = false
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(42L, row.CLPKID);
|
||||
Assert.Equal("Ville de Lyon", row.CLNom);
|
||||
Assert.Equal("C001", row.CLCodeClient);
|
||||
Assert.True(row.CLBlocage);
|
||||
Assert.False(row.Archive);
|
||||
}
|
||||
|
||||
// ---- 2. ClientRow Omika minimal ------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_OmikaClientRow_Minimal_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 7L,
|
||||
["CL_Nom"] = "Mairie de Paris",
|
||||
["CL_CleType"] = "COLLECTIVITE",
|
||||
["CL_CleStatut"] = "ACTIF",
|
||||
["GUID"] = "abc-123"
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<OmikaClientRow>(values);
|
||||
|
||||
Assert.Equal(7L, row.CLPKID);
|
||||
Assert.Equal("Mairie de Paris", row.CLNom);
|
||||
Assert.Equal("COLLECTIVITE", row.CLCleType);
|
||||
Assert.Equal("ACTIF", row.CLCleStatut);
|
||||
Assert.Equal("abc-123", row.GUID);
|
||||
}
|
||||
|
||||
// ---- 3. Colonne numérique long -------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_LongColumn_MapsValue()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 123456789L,
|
||||
["CL_IDType"] = 5L
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(123456789L, row.CLPKID);
|
||||
Assert.Equal(5L, row.CLIDType);
|
||||
}
|
||||
|
||||
// ---- 4. String -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_StringColumn_MapsValue()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Nom"] = "Entreprise Dupont",
|
||||
["CL_Email"] = "contact@dupont.fr"
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal("Entreprise Dupont", row.CLNom);
|
||||
Assert.Equal("contact@dupont.fr", row.CLEmail);
|
||||
}
|
||||
|
||||
// ---- 5. Bool natif -------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_BoolNative_MapsValue()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Blocage"] = true,
|
||||
["Archive"] = false
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.True(row.CLBlocage);
|
||||
Assert.False(row.Archive);
|
||||
}
|
||||
|
||||
// ---- 6. Bool depuis 0 ----------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_BoolFromZero_IsFalse()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Blocage"] = 0L
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.False(row.CLBlocage);
|
||||
}
|
||||
|
||||
// ---- 7. Bool depuis 1 ----------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_BoolFromOne_IsTrue()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Blocage"] = 1L
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.True(row.CLBlocage);
|
||||
}
|
||||
|
||||
// ---- 8. Null vers propriété nullable/référence --------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_NullToNullable_IsNull()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Nom"] = null,
|
||||
["CL_Email"] = null
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Null(row.CLNom);
|
||||
Assert.Null(row.CLEmail);
|
||||
}
|
||||
|
||||
// ---- 9. Null vers valeur non nullable => défaut -------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_NullToNonNullable_KeepsDefault()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = null,
|
||||
["CL_Blocage"] = null
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(0L, row.CLPKID);
|
||||
Assert.False(row.CLBlocage);
|
||||
}
|
||||
|
||||
// ---- 10. Colonne absente => propriété par défaut ------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_MissingColumn_KeepsDefault()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 1L
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(1L, row.CLPKID);
|
||||
Assert.Null(row.CLNom);
|
||||
Assert.Null(row.CLCodeClient);
|
||||
Assert.False(row.CLBlocage);
|
||||
}
|
||||
|
||||
// ---- 11. Mapping insensible à la casse ----------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_ColumnNameCaseInsensitive()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["cl_pkid"] = 9L,
|
||||
["CL_NOM"] = "Casé",
|
||||
["Cl_Blocage"] = true
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(9L, row.CLPKID);
|
||||
Assert.Equal("Casé", row.CLNom);
|
||||
Assert.True(row.CLBlocage);
|
||||
}
|
||||
|
||||
// ---- 12. Colonnes supplémentaires inconnues => ignorées -----------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_UnknownColumns_AreIgnored()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 1L,
|
||||
["COLONNE_INCONNUE"] = "valeur",
|
||||
["AUTRE"] = 123
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(1L, row.CLPKID);
|
||||
}
|
||||
|
||||
// ---- 13. Valeur numérique convertible vers le type cible ----------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_NumericConvertible_ConvertsToTargetType()
|
||||
{
|
||||
// CL_PKID est long : une valeur int doit être convertie.
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 42,
|
||||
["CL_IDType"] = 3
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(42L, row.CLPKID);
|
||||
Assert.Equal(3L, row.CLIDType);
|
||||
}
|
||||
|
||||
// ---- 14. Type incompatible => exception explicite -----------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_IncompatibleType_ThrowsExplicitException()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = "pas-un-nombre"
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => HfsqlRowMapper.Map<BatikClientRow>(values));
|
||||
|
||||
Assert.Contains("CL_PKID", ex.Message);
|
||||
Assert.Contains("ClientRow", ex.Message);
|
||||
Assert.Contains("CLPKID", ex.Message);
|
||||
}
|
||||
|
||||
// ---- 15. Aucun contenu sensible de valeur dans l'exception --------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_Exception_DoesNotContainSensitiveValue()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = "mot-de-passe-secret"
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => HfsqlRowMapper.Map<BatikClientRow>(values));
|
||||
|
||||
Assert.DoesNotContain("mot-de-passe-secret", ex.Message);
|
||||
}
|
||||
|
||||
// ---- 16. Row avec champs HfSqlLegacyField => mappés normalement ---------------
|
||||
|
||||
[Fact]
|
||||
public void Map_LegacyFields_AreMappedNormally()
|
||||
{
|
||||
// Omika.ClientRow a des champs zombie/legacy (CL_IDType, CL_IDStatut).
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 5L,
|
||||
["CL_IDType"] = 2L,
|
||||
["CL_IDStatut"] = 1L
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<OmikaClientRow>(values);
|
||||
|
||||
Assert.Equal(5L, row.CLPKID);
|
||||
Assert.Equal(2L, row.CLIDType);
|
||||
Assert.Equal(1L, row.CLIDStatut);
|
||||
}
|
||||
|
||||
// ---- 17. AdresseRow ------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_AdresseRow_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["AD_PKID"] = 10L,
|
||||
["AD_Etiquette"] = "Mairie",
|
||||
["AD_Ligne_1"] = "1 place de la République",
|
||||
["AD_CodePostal"] = "69001",
|
||||
["AD_Ville"] = "Lyon"
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikAdresseRow>(values);
|
||||
|
||||
Assert.Equal(10L, row.ADPKID);
|
||||
Assert.Equal("Mairie", row.ADEtiquette);
|
||||
Assert.Equal("1 place de la République", row.ADLigne1);
|
||||
Assert.Equal("69001", row.ADCodePostal);
|
||||
Assert.Equal("Lyon", row.ADVille);
|
||||
}
|
||||
|
||||
// ---- 18. PersonneRow -----------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_PersonneRow_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["PE_PKID"] = 20L,
|
||||
["PE_Nom"] = "Dupont",
|
||||
["PE_Prenom"] = "Jean",
|
||||
["PE_Email"] = "jean.dupont@mail.fr",
|
||||
["PE_Actif"] = true
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikPersonneRow>(values);
|
||||
|
||||
Assert.Equal(20L, row.PEPKID);
|
||||
Assert.Equal("Dupont", row.PENom);
|
||||
Assert.Equal("Jean", row.PEPrenom);
|
||||
Assert.Equal("jean.dupont@mail.fr", row.PEEmail);
|
||||
Assert.True(row.PEActif);
|
||||
}
|
||||
|
||||
// ---- 19. LieuRow ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_LieuRow_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["LI_PKID"] = 30L,
|
||||
["LI_CodeLieu"] = "SITE-01",
|
||||
["LI_IDAdresse"] = 10L,
|
||||
["LI_IDClientPrincipal"] = 42L,
|
||||
["LI_Blocage"] = false
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikLieuRow>(values);
|
||||
|
||||
Assert.Equal(30L, row.LIPKID);
|
||||
Assert.Equal("SITE-01", row.LICodeLieu);
|
||||
Assert.Equal(10L, row.LIIDAdresse);
|
||||
Assert.Equal(42L, row.LIIDClientPrincipal);
|
||||
Assert.False(row.LIBlocage);
|
||||
}
|
||||
|
||||
// ---- 20. ParametreMetierRow ----------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_ParametreMetierRow_MapsCorrectly()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["PA_PKID"] = 100L,
|
||||
["PA_Code"] = "TYP",
|
||||
["PA_Libelle"] = "Type de client",
|
||||
["PA_Valeur"] = "01",
|
||||
["PA_Ordre"] = 3,
|
||||
["PA_Actif"] = true
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikParametreMetierRow>(values);
|
||||
|
||||
Assert.Equal(100L, row.PAPKID);
|
||||
Assert.Equal("TYP", row.PACode);
|
||||
Assert.Equal("Type de client", row.PALibelle);
|
||||
Assert.Equal("01", row.PAValeur);
|
||||
Assert.Equal(3, row.PAOrdre);
|
||||
Assert.True(row.PAActif);
|
||||
}
|
||||
|
||||
// ---- 21. Plusieurs appels successifs produisent le même résultat --------------
|
||||
|
||||
[Fact]
|
||||
public void Map_RepeatedCalls_AreDeterministic()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 42L,
|
||||
["CL_Nom"] = "Ville de Lyon"
|
||||
};
|
||||
|
||||
var first = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
var second = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(first.CLPKID, second.CLPKID);
|
||||
Assert.Equal(first.CLNom, second.CLNom);
|
||||
}
|
||||
|
||||
// ---- 22. Utilisation concurrente sans corruption ------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_ConcurrentCalls_AreSafe()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 42L,
|
||||
["CL_Nom"] = "Ville de Lyon"
|
||||
};
|
||||
|
||||
var results = new BatikClientRow[100];
|
||||
Parallel.For(0, results.Length, i =>
|
||||
{
|
||||
results[i] = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
});
|
||||
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.Equal(42L, row.CLPKID);
|
||||
Assert.Equal("Ville de Lyon", row.CLNom);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 23. Aucune dépendance à ODBC dans l'API publique -------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_PublicSignature_DoesNotExposeOdbc()
|
||||
{
|
||||
var method = typeof(HfsqlRowMapper).GetMethod(nameof(HfsqlRowMapper.Map))!;
|
||||
|
||||
Assert.DoesNotContain("Odbc", method.ReturnType.FullName, StringComparison.Ordinal);
|
||||
foreach (var parameter in method.GetParameters())
|
||||
{
|
||||
Assert.DoesNotContain("Odbc", parameter.ParameterType.FullName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 24. Aucun type canonique Client/Adresse/Contact/Site dans l'API publique --
|
||||
|
||||
[Fact]
|
||||
public void Map_PublicSignature_DoesNotExposeCanonicalTypes()
|
||||
{
|
||||
var method = typeof(HfsqlRowMapper).GetMethod(nameof(HfsqlRowMapper.Map))!;
|
||||
|
||||
Assert.DoesNotContain("Client", method.ReturnType.FullName, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Adresse", method.ReturnType.FullName, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Contact", method.ReturnType.FullName, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Site", method.ReturnType.FullName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ---- 25. DBNull.Value défensif -------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_DbNull_IsTreatedAsNull()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_Nom"] = DBNull.Value,
|
||||
["CL_PKID"] = DBNull.Value
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Null(row.CLNom);
|
||||
Assert.Equal(0L, row.CLPKID);
|
||||
}
|
||||
|
||||
// ---- 26. DateTime --------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_DateTimeColumn_MapsValue()
|
||||
{
|
||||
var date = new DateTime(2024, 5, 1, 10, 30, 0);
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 1L,
|
||||
["CreationDate"] = date
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(date, row.CreationDate);
|
||||
}
|
||||
|
||||
// ---- 27. Valeur nulle vers propriété valeur non nullable ----------------------
|
||||
|
||||
[Fact]
|
||||
public void Map_NullToValueType_KeepsDefault()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = null,
|
||||
["CL_IDType"] = null
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(0L, row.CLPKID);
|
||||
Assert.Equal(0L, row.CLIDType);
|
||||
}
|
||||
|
||||
// ---- 28. Chaîne NUL-paddée (rubrique « Mémo texte » / largeur fixe) ------------
|
||||
|
||||
[Fact]
|
||||
public void Map_StringWithNulPadding_IsNormalized()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 1L,
|
||||
["CL_Nom"] = "Ville de Lyon\u0000\u0000\u0000",
|
||||
["CL_CodeClient"] = "C001\u0000\u0000"
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal("Ville de Lyon", row.CLNom);
|
||||
Assert.Equal("C001", row.CLCodeClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_StringAllNul_IsNormalizedToEmpty()
|
||||
{
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["CL_PKID"] = 1L,
|
||||
["CL_Nom"] = "\u0000\u0000\u0000"
|
||||
};
|
||||
|
||||
var row = HfsqlRowMapper.Map<BatikClientRow>(values);
|
||||
|
||||
Assert.Equal(string.Empty, row.CLNom);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user