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; /// /// Tests unitaires de : 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. /// public class HfsqlRowMapperTests { // ---- 1. ClientRow Batik minimal ------------------------------------------------ [Fact] public void Map_BatikClientRow_Minimal_MapsCorrectly() { var values = new Dictionary { ["CL_PKID"] = 42L, ["CL_Nom"] = "Ville de Lyon", ["CL_CodeClient"] = "C001", ["CL_Blocage"] = true, ["Archive"] = false }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 7L, ["CL_Nom"] = "Mairie de Paris", ["CL_CleType"] = "COLLECTIVITE", ["CL_CleStatut"] = "ACTIF", ["GUID"] = "abc-123" }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 123456789L, ["CL_IDType"] = 5L }; var row = HfsqlRowMapper.Map(values); Assert.Equal(123456789L, row.CLPKID); Assert.Equal(5L, row.CLIDType); } // ---- 4. String ----------------------------------------------------------------- [Fact] public void Map_StringColumn_MapsValue() { var values = new Dictionary { ["CL_Nom"] = "Entreprise Dupont", ["CL_Email"] = "contact@dupont.fr" }; var row = HfsqlRowMapper.Map(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 { ["CL_Blocage"] = true, ["Archive"] = false }; var row = HfsqlRowMapper.Map(values); Assert.True(row.CLBlocage); Assert.False(row.Archive); } // ---- 6. Bool depuis 0 ---------------------------------------------------------- [Fact] public void Map_BoolFromZero_IsFalse() { var values = new Dictionary { ["CL_Blocage"] = 0L }; var row = HfsqlRowMapper.Map(values); Assert.False(row.CLBlocage); } // ---- 7. Bool depuis 1 ---------------------------------------------------------- [Fact] public void Map_BoolFromOne_IsTrue() { var values = new Dictionary { ["CL_Blocage"] = 1L }; var row = HfsqlRowMapper.Map(values); Assert.True(row.CLBlocage); } // ---- 8. Null vers propriété nullable/référence -------------------------------- [Fact] public void Map_NullToNullable_IsNull() { var values = new Dictionary { ["CL_Nom"] = null, ["CL_Email"] = null }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = null, ["CL_Blocage"] = null }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 1L }; var row = HfsqlRowMapper.Map(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 { ["cl_pkid"] = 9L, ["CL_NOM"] = "Casé", ["Cl_Blocage"] = true }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 1L, ["COLONNE_INCONNUE"] = "valeur", ["AUTRE"] = 123 }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 42, ["CL_IDType"] = 3 }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = "pas-un-nombre" }; var ex = Assert.Throws( () => HfsqlRowMapper.Map(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 { ["CL_PKID"] = "mot-de-passe-secret" }; var ex = Assert.Throws( () => HfsqlRowMapper.Map(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 { ["CL_PKID"] = 5L, ["CL_IDType"] = 2L, ["CL_IDStatut"] = 1L }; var row = HfsqlRowMapper.Map(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 { ["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(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 { ["PE_PKID"] = 20L, ["PE_Nom"] = "Dupont", ["PE_Prenom"] = "Jean", ["PE_Email"] = "jean.dupont@mail.fr", ["PE_Actif"] = true }; var row = HfsqlRowMapper.Map(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 { ["LI_PKID"] = 30L, ["LI_CodeLieu"] = "SITE-01", ["LI_IDAdresse"] = 10L, ["LI_IDClientPrincipal"] = 42L, ["LI_Blocage"] = false }; var row = HfsqlRowMapper.Map(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 { ["PA_PKID"] = 100L, ["PA_Code"] = "TYP", ["PA_Libelle"] = "Type de client", ["PA_Valeur"] = "01", ["PA_Ordre"] = 3, ["PA_Actif"] = true }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 42L, ["CL_Nom"] = "Ville de Lyon" }; var first = HfsqlRowMapper.Map(values); var second = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 42L, ["CL_Nom"] = "Ville de Lyon" }; var results = new BatikClientRow[100]; Parallel.For(0, results.Length, i => { results[i] = HfsqlRowMapper.Map(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 { ["CL_Nom"] = DBNull.Value, ["CL_PKID"] = DBNull.Value }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 1L, ["CreationDate"] = date }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = null, ["CL_IDType"] = null }; var row = HfsqlRowMapper.Map(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 { ["CL_PKID"] = 1L, ["CL_Nom"] = "Ville de Lyon\u0000\u0000\u0000", ["CL_CodeClient"] = "C001\u0000\u0000" }; var row = HfsqlRowMapper.Map(values); Assert.Equal("Ville de Lyon", row.CLNom); Assert.Equal("C001", row.CLCodeClient); } [Fact] public void Map_StringAllNul_IsNormalizedToEmpty() { var values = new Dictionary { ["CL_PKID"] = 1L, ["CL_Nom"] = "\u0000\u0000\u0000" }; var row = HfsqlRowMapper.Map(values); Assert.Equal(string.Empty, row.CLNom); } }