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);
}
}