using System.Reflection; using Omika.Data.HfSql.Metadata; using Omika.Data.HfSql.Models.Omika; using Omika.Data.Repositories.Omika; namespace Omika.Tests.Repositories; /// /// Garde-fou structurel : chaque colonne référencée par les SELECT des /// repositories Omika doit exister dans le modèle physique de la Row cible /// (attribut ). /// /// Contexte : la recherche mobile (lot 36C) échouait en HTTP 500 sur /// OMIKA_TEST car sélectionnait des /// colonnes inexistantes dans le schéma Omika (ex. RVC_IDRdv, RVCT_IDRdv, /// RVC_GUIDColb au lieu de RVC_GUID_RDV, RVC_GUID_Colb...). Ce test reproduit /// ce défaut sans base réelle : il vérifie la cohérence SQL ↔ modèle. /// public class OmikaSqlColumnConsistencyTests { public static IEnumerable Cases() { // Recherche mobile (enrichissement) : rendez-vous, clients, sites, interventions. yield return [typeof(OmikaRendezVousRepository), "SqlSelectRendezVousColumns", typeof(RDVPlanningRow)]; yield return [typeof(OmikaRendezVousRepository), "SqlSelectCollaborateurColumns", typeof(RDVCollaborateurRow)]; yield return [typeof(OmikaRendezVousRepository), "SqlSelectContactColumns", typeof(RDVContactRow)]; yield return [typeof(OmikaRendezVousRepository), "SqlSelectParametreMetierColumns", typeof(ParametreMetierRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectClientColumns", typeof(ClientRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectAdresseColumns", typeof(AdresseRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectLieuColumns", typeof(LieuRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectPersonneColumns", typeof(PersonneRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectParametreMetierColumns", typeof(ParametreMetierRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectClientAdresseColumns", typeof(ClientAdresseRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectClientLieuColumns", typeof(ClientLieuRow)]; yield return [typeof(OmikaClientRepository), "SqlSelectLieuPersonneColumns", typeof(LieuPersonneRow)]; yield return [typeof(OmikaSiteRepository), "SqlSelectLieuColumns", typeof(LieuRow)]; yield return [typeof(OmikaInterventionRepository), "SqlSelectInterventionColumns", typeof(InterventionRow)]; yield return [typeof(OmikaInterventionRepository), "SqlSelectParametreMetierColumns", typeof(ParametreMetierRow)]; yield return [typeof(OmikaInterventionRepository), "SqlSelectInterventionPersonneColumns", typeof(InterventionPersonneRow)]; yield return [typeof(OmikaInterventionRepository), "SqlSelectEtatInterventionColumns", typeof(EtatInterventionRow)]; } [Theory] [MemberData(nameof(Cases))] public void ColonnesSelect_ExistentDansLeModelePhysique(Type repositoryType, string fieldName, Type rowType) { string sql = GetPrivateConstString(repositoryType, fieldName); var physicalColumns = GetPhysicalColumns(rowType); foreach (string column in ParseColumns(sql)) { Assert.True( physicalColumns.Contains(column, StringComparer.OrdinalIgnoreCase), $"{repositoryType.Name}.{fieldName} référence la colonne « {column} » absente du modèle {rowType.Name}."); } } [Fact] public void RendezVousRepository_GetDetailsAsync_NUtilisePasDeColonneNumeriqueFantome() { string source = ReadRendezVousRepositorySource(); // Colonnes de liaison numériques fantômes interdites : elles proviennent // d'une transposition incorrecte du modèle Batik (*_IDRdv) alors que le // modèle Omika relie les tables satellites du rendez-vous par GUID. Assert.DoesNotContain("RVC_IDRdv", source, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("RVCT_IDRdv", source, StringComparison.OrdinalIgnoreCase); // GetDetailsAsync doit adresser RDV_Collaborateur et RDV_Contact par leur // GUID de liaison (RVC_GUID_RDV / RVCT_GUID_RDV), avec le GUID du // rendez-vous racine (rendezVousRow.GUID) — exactement comme SearchDetailsAsync. // Les affectations ARCHIVÉES sont exclues : un collaborateur retiré du // rendez-vous ne fait plus partie du groupe. Assert.Contains("FROM RDV_Collaborateur", source, StringComparison.Ordinal); Assert.Contains("WHERE RVC_GUID_RDV = ? AND Archive = ?", source, StringComparison.Ordinal); Assert.Contains("FROM RDV_Contact WHERE RVCT_GUID_RDV = ?", source, StringComparison.Ordinal); Assert.Contains("[rendezVousRow.GUID, false]", source, StringComparison.Ordinal); } private static string ReadRendezVousRepositorySource() { string baseDir = AppDomain.CurrentDomain.BaseDirectory; var dir = new DirectoryInfo(baseDir); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Omika.slnx"))) { dir = dir.Parent; } string root = dir?.FullName ?? baseDir; return File.ReadAllText(Path.Combine( root, "src", "Omika.Data", "Repositories", "Omika", "OmikaRendezVousRepository.cs")); } private static string GetPrivateConstString(Type repositoryType, string fieldName) { FieldInfo? field = repositoryType.GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); Assert.True(field is not null, $"Champ « {fieldName} » introuvable dans {repositoryType.Name}."); Assert.True(field!.IsLiteral, $"{repositoryType.Name}.{fieldName} n'est pas une constante compile-time."); string? value = field.GetRawConstantValue() as string; Assert.False(string.IsNullOrWhiteSpace(value), $"{repositoryType.Name}.{fieldName} est vide."); return value!; } private static HashSet GetPhysicalColumns(Type rowType) { var columns = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (PropertyInfo property in rowType.GetProperties()) { HfSqlColumnAttribute? attr = property.GetCustomAttribute(); if (attr is not null) { columns.Add(attr.ColumnName); } } Assert.NotEmpty(columns); return columns; } private static IEnumerable ParseColumns(string sql) => sql .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); }