using Omika.Core.Compatibility; using Omika.Core.Domain.Entities.Clients; using Omika.Core.Domain.ValueObjects; using Omika.Data.Mapping.Common; using OmikaModels = Omika.Data.HfSql.Models.Omika; using OmikaMappers = Omika.Data.Mapping.Omika; namespace Omika.Tests.Mapping; /// /// Tests unitaires du mapper Omika → canonique des contacts (table Personne). /// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire. /// public class OmikaContactMapperTests { // ---- 1. Contact Omika minimal -------------------------------------------------- [Fact] public void ContactOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue() { var row = new OmikaModels.PersonneRow { PEPKID = 1 }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion); Assert.NotNull(result.Value); Assert.Null(result.Value.Nom); Assert.Null(result.Value.Prenom); Assert.Null(result.Value.Civilite); Assert.Empty(result.Value.Roles); Assert.Null(result.Value.Coordonnees); Assert.False(result.Value.EstArchive); Assert.NotNull(result.Value.SourceReference); Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source); Assert.Equal(1, result.Value.SourceReference!.NumericId); Assert.Null(result.Value.SourceReference!.EntityGuid); Assert.Empty(result.Warnings); } // ---- 2. Contact Omika complet -------------------------------------------------- [Fact] public void ContactOmika_Full_MapsAllCanonicalProperties() { var row = new OmikaModels.PersonneRow { PEPKID = 42, GUID = "550e8400-e29b-41d4-a716-446655440000", PECleCivilite = "MR", PEIDCivilite = 2, PENom = "Dupont", PEPrenom = "Jean", PECleRole = "R_CP", PEIDRole = 3, PETelFixe = "0472000000", PETelMobile = "0600000000", PEEmail = "jean.dupont@ville-lyon.fr", PEFax = "0472000001", PEIDClient = 10, PEIDAdressePrincipale = 20, PENote = "Contact principal", PEActif = true, PEUsageInterne = false, PEInfoSupp = "Disponible en journée", Archive = true, PEIDImport = "IMP-O-42" }; var referenceRows = new Dictionary { ["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" }, ["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Contact principal", PACategorie = "ROLE" } }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows); Contact contact = result.Value; Assert.Equal("M.", contact.Civilite!.Label); Assert.Equal("MR", contact.Civilite!.Key); Assert.Equal(2, contact.Civilite!.LegacyId); Assert.Equal("Dupont", contact.Nom); Assert.Equal("Jean", contact.Prenom); Assert.Equal("Jean Dupont", contact.NomAffiche); Assert.Single(contact.Roles); Assert.Equal("Contact principal", contact.Roles[0].Label); Assert.Equal("R_CP", contact.Roles[0].Key); Assert.Equal(3, contact.Roles[0].LegacyId); Assert.Equal("jean.dupont@ville-lyon.fr", contact.Coordonnees!.Email); Assert.Equal("0472000000", contact.Coordonnees!.Phone); Assert.Equal("0600000000", contact.Coordonnees!.Mobile); Assert.Equal("0472000001", contact.Coordonnees!.Fax); Assert.True(contact.Actif); Assert.True(contact.EstArchive); Assert.Equal("Disponible en journée", contact.InformationsSupplementaires); Assert.Equal(42, contact.SourceReference!.NumericId); Assert.Equal("IMP-O-42", contact.SourceReference!.ImportId); Assert.Equal(DatabaseSchemaVersion.Omika, contact.SourceReference!.Source); Assert.NotNull(contact.SourceReference!.EntityGuid); } // ---- 3. GUID valide avec tirets ------------------------------------------------- [Fact] public void ContactOmika_ValidGuidWithDashes_IsKeptInEntityGuid() { var row = new OmikaModels.PersonneRow { PEPKID = 7, GUID = "550e8400-e29b-41d4-a716-446655440000", PENom = "Dupont" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.SourceReference!.EntityGuid); Assert.Equal( "550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.EntityGuid!.Value.ToString("D")); Assert.Equal("550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.PhysicalGuid); Assert.Empty(result.Warnings); } // ---- 4. GUID valide sans tirets ------------------------------------------------- [Fact] public void ContactOmika_ValidGuidWithoutDashes_IsParsedByTryParse() { var row = new OmikaModels.PersonneRow { PEPKID = 8, GUID = "550e8400e29b41d4a716446655440000" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.SourceReference!.EntityGuid); Assert.Equal( "550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.EntityGuid!.Value.ToString("D")); Assert.Empty(result.Warnings); } // ---- 5. GUID invalide ----------------------------------------------------------- [Fact] public void ContactOmika_InvalidGuid_ProducesNullGuidAndWarningWithoutException() { var row = new OmikaModels.PersonneRow { PEPKID = 9, GUID = "not-a-guid", PENom = "Société inconnue" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Null(result.Value.SourceReference!.EntityGuid); Assert.Equal("not-a-guid", result.Value.SourceReference!.PhysicalGuid); Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.InvalidPhysicalGuid, StringComparison.Ordinal)); Assert.Equal("Société inconnue", result.Value.Nom); } // ---- 6. Coordonnées entièrement nulles ------------------------------------------- [Fact] public void ContactOmika_EmptyCoordinates_ProducesNullCoordonnees() { var row = new OmikaModels.PersonneRow { PEPKID = 5, PENom = "Martin", PEEmail = " ", PETelFixe = "", PETelMobile = null, PEFax = "\t" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Null(result.Value.Coordonnees); Assert.Equal("Martin", result.Value.Nom); } // ---- 7. Coordonnées partielles --------------------------------------------------- [Fact] public void ContactOmika_PartialCoordinates_KeepsOnlyProvidedValues() { var row = new OmikaModels.PersonneRow { PEPKID = 6, PEEmail = "m@exemple.fr", PETelFixe = null, PETelMobile = "0600000000", PEFax = "" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.Coordonnees); Assert.Equal("m@exemple.fr", result.Value.Coordonnees!.Email); Assert.Null(result.Value.Coordonnees!.Phone); Assert.Equal("0600000000", result.Value.Coordonnees!.Mobile); Assert.Null(result.Value.Coordonnees!.Fax); } // ---- 8. Priorité de la clé métier sur l'ancien identifiant ------------------------ [Fact] public void ContactOmika_BusinessKeyIsPreferredAndLegacyIdIsKeptAsCompatibility() { var referenceRows = new Dictionary { ["MR"] = new() { PAPKID = 50, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" } }; var row = new OmikaModels.PersonneRow { PEPKID = 13, PECleCivilite = "MR", PEIDCivilite = 99 // zombie : ancien identifiant }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows); Assert.Equal("MR", result.Value.Civilite!.Key); Assert.Equal("M.", result.Value.Civilite!.Label); Assert.Equal(99, result.Value.Civilite!.LegacyId); Assert.Empty(result.Warnings); } // ---- 9. Conservation de l'identifiant numérique historique ------------------------- [Fact] public void ContactOmika_NumericIdentifier_IsPreserved() { var row = new OmikaModels.PersonneRow { PEPKID = 1234 }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.SourceReference); Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source); Assert.Equal(1234, result.Value.SourceReference!.NumericId); } // ---- 10. Conservation de l'identifiant d'import ------------------------------------- [Fact] public void ContactOmika_ImportId_IsPreserved() { var row = new OmikaModels.PersonneRow { PEPKID = 5678, PEIDImport = "IMP-O" }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Equal("IMP-O", result.Value.SourceReference!.ImportId); Assert.Equal(5678, result.Value.SourceReference!.NumericId); } // ---- 11. Absence de GUID fabriqué ----------------------------------------------------- [Fact] public void ContactOmika_WithoutGuid_ProducesNoFabricatedGuid() { var row = new OmikaModels.PersonneRow { PEPKID = 11 }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Null(result.Value.SourceReference!.EntityGuid); Assert.Null(result.Value.SourceReference!.PhysicalGuid); Assert.Empty(result.Warnings); } // ---- 12. Repli sur un champ zombie avec avertissement -------------------------------- [Fact] public void ContactOmika_MissingBusinessKey_ExposesZombieLegacyIdWithWarning() { var row = new OmikaModels.PersonneRow { PEPKID = 14, PECleCivilite = null, // rubrique active absente PEIDCivilite = 77 // ancien identifiant (zombie) }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.Civilite); Assert.Equal(77, result.Value.Civilite!.LegacyId); Assert.Null(result.Value.Civilite!.Key); Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal)); } // ---- 13. Référentiel non résolu --------------------------------------------------------- [Fact] public void ContactOmika_UnresolvedBusinessKey_KeepsKeyAndLegacyIdAndWarns() { var row = new OmikaModels.PersonneRow { PEPKID = 15, PECleCivilite = "MR", PEIDCivilite = 77 }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value.Civilite); Assert.Equal("MR", result.Value.Civilite!.Key); Assert.Equal(77, result.Value.Civilite!.LegacyId); Assert.Equal(string.Empty, result.Value.Civilite!.Label); Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal)); } [Fact] public void ContactOmika_ZeroCiviliteAndRole_ProducesNoReference() { var row = new OmikaModels.PersonneRow { PEPKID = 16, PEIDCivilite = 0, PEIDRole = 0 }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Null(result.Value.Civilite); Assert.Empty(result.Value.Roles); Assert.Empty(result.Warnings); } // ---- 14. Chaînes vides sans exception ----------------------------------------------------- [Fact] public void ContactOmika_EmptyOrNullStrings_NeverThrow() { var row = new OmikaModels.PersonneRow { PENom = "", PEPrenom = " ", PEEmail = "", PETelFixe = null, PETelMobile = " ", PEFax = "", PENote = "", PEInfoSupp = " ", PEIDImport = "", GUID = null }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value); Assert.Null(result.Value.Nom); Assert.Null(result.Value.Prenom); Assert.Null(result.Value.Coordonnees); Assert.Null(result.Value.InformationsSupplementaires); Assert.Empty(result.Warnings); } [Fact] public void ContactOmika_DefaultRow_NeverThrows() { var row = new OmikaModels.PersonneRow(); CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.NotNull(result.Value); Assert.Null(result.Value.SourceReference!.NumericId); Assert.False(result.Value.Actif); Assert.False(result.Value.EstArchive); } // ---- 15. Champs sans équivalent dans IgnoredFields ---------------------------------------- [Fact] public void ContactOmika_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored() { var row = new OmikaModels.PersonneRow { PEPKID = 9, PEIDClient = 10, PEIDAdressePrincipale = 20, PENote = "note", PEUsageInterne = true, ModifDate = new DateTime(2024, 1, 1), CreationDate = new DateTime(2023, 1, 1), DateArchivage = new DateTime(2024, 6, 1) }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row); Assert.Contains(result.IgnoredFields, f => f == "PE_IDClient"); Assert.Contains(result.IgnoredFields, f => f == "PE_IDAdressePrincipale"); Assert.Contains(result.IgnoredFields, f => f == "PE_Note"); Assert.Contains(result.IgnoredFields, f => f == "PE_UsageInterne"); Assert.Contains(result.IgnoredFields, f => f == "ModifDate"); Assert.Contains(result.IgnoredFields, f => f == "CreationDate"); Assert.Contains(result.IgnoredFields, f => f == "DateArchivage"); Assert.Null(result.Value.ClientId); Assert.Empty(result.Value.SiteIds); } // ---- 16. Absence de noms physiques HFSQL dans le domaine canonique ------------------------- [Fact] public void ContactOmika_CanonicalDomain_DoesNotExposeHfSqlNames() { var row = new OmikaModels.PersonneRow { PEPKID = 40, GUID = "550e8400-e29b-41d4-a716-446655440000", PECleCivilite = "MR", PEIDCivilite = 2, PENom = "Dupont", PEPrenom = "Jean", PECleRole = "R_CP", PEIDRole = 3, PEEmail = "j@exemple.fr", PETelFixe = "01", PETelMobile = "06", PEFax = "02", PEIDClient = 10, PEIDAdressePrincipale = 20, PENote = "note", PEActif = true, PEUsageInterne = true, PEInfoSupp = "info", Archive = false, PEIDImport = "IMP-40" }; var referenceRows = new Dictionary { ["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" }, ["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Rôle", PACategorie = "ROLE" } }; CanonicalMappingResult result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows); Contact contact = result.Value; string[] forbidden = [ "PE_", "CL_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "GUID" ]; AssertHasNoPhysicalNames(contact, forbidden); if (contact.Coordonnees is not null) { AssertHasNoPhysicalNames(contact.Coordonnees, forbidden); } if (contact.SourceReference is not null) { AssertHasNoPhysicalNames(contact.SourceReference, forbidden); } } // ---- Helper --------------------------------------------------------------------- private static void AssertHasNoPhysicalNames(object value, string[] forbidden) { foreach (var property in value.GetType().GetProperties()) { Assert.DoesNotContain(forbidden, f => property.Name.Contains(f, StringComparison.Ordinal)); } } }