Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,467 @@
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;
/// <summary>
/// Tests unitaires du mapper Omika → canonique des contacts (table Personne).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class OmikaContactMapperTests
{
// ---- 1. Contact Omika minimal --------------------------------------------------
[Fact]
public void ContactOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new OmikaModels.PersonneRow { PEPKID = 1 };
CanonicalMappingResult<Contact> 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<string, OmikaModels.ParametreMetierRow>
{
["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" },
["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Contact principal", PACategorie = "ROLE" }
};
CanonicalMappingResult<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<string, OmikaModels.ParametreMetierRow>
{
["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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<Contact> 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<string, OmikaModels.ParametreMetierRow>
{
["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" },
["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Rôle", PACategorie = "ROLE" }
};
CanonicalMappingResult<Contact> 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));
}
}
}