using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Domain.Commands; using Omika.Core.Services.Mobile; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.HfSql.Metadata; using Omika.Data.Repositories.Batik; namespace Omika.Tests.Repositories.Batik; /// /// Tests unitaires de la création d'un compte rendu Batik /// () centrés sur la colonne /// CR_IDCollaborateur : aucun serveur HFSQL, aucun ODBC réel, aucun réseau. /// Un fake enregistre le SQL et les paramètres, et /// retourne des dictionnaires selon la table interrogée. /// /// Objectifs : /// /// la colonne porte l'identité du collaborateur AUTEUR du compte rendu, et /// non plus systématiquement le collaborateur affecté à Planning_RDV ; /// le PK numérique du jeton mobile est repris tel quel, et le GUID /// synthétique réversible Batik est décodé (la table Collaborateur Batik /// ne porte aucune colonne GUID) ; /// à défaut d'auteur résolvable, le collaborateur affecté au rendez-vous /// reste le repli historique : la colonne n'est jamais laissée vide. /// /// public class BatikCompteRenduRepositoryTests { private const long RendezVousId = 42L; private const long CollaborateurDuRendezVous = 7L; [Fact] public async Task CreateAsync_AuteurNumerique_RenseigneLAuteurEtNonLeCollaborateurDuRendezVous() { var connection = new FakeHfsqlConnection { PlanningRows = [PlanningRow()], CompteRenduRows = [CompteRenduRow()] }; var repository = new BatikCompteRenduRepository(connection); await repository.CreateAsync("42", Command("9")); ExecutedCommand insert = Insert(connection); // 9 (auteur) et non 7 (collaborateur affecté à Planning_RDV). Assert.Equal(9L, CrIdCollaborateur(insert)); } [Fact] public async Task CreateAsync_AuteurGuidSynthetique_DecodeLePkNumerique() { var connection = new FakeHfsqlConnection { PlanningRows = [PlanningRow()], CompteRenduRows = [CompteRenduRow()] }; var repository = new BatikCompteRenduRepository(connection); // Identité publique Batik : GUID synthétique réversible dérivé du CO_PKID 55. Guid auteurSynthetique = MobileEntityIdentityResolver.EncodeSyntheticIdentity( SyncEntityTypes.Collaborateur, 55L); await repository.CreateAsync("42", Command(auteurSynthetique.ToString("D"))); ExecutedCommand insert = Insert(connection); Assert.Equal(55L, CrIdCollaborateur(insert)); } [Fact] public async Task CreateAsync_SansAuteur_RetombeSurLeCollaborateurDuRendezVous() { var connection = new FakeHfsqlConnection { PlanningRows = [PlanningRow()], CompteRenduRows = [CompteRenduRow()] }; var repository = new BatikCompteRenduRepository(connection); await repository.CreateAsync("42", Command(collaborateurId: null)); ExecutedCommand insert = Insert(connection); Assert.Equal(CollaborateurDuRendezVous, CrIdCollaborateur(insert)); } [Fact] public async Task CreateAsync_AuteurNonResolvable_RetombeSurLeCollaborateurDuRendezVous() { // GUID non canonique (ni PK numérique, ni GUID synthétique réversible) : // la colonne est remplie avec le repli plutôt que laissée à 0. var connection = new FakeHfsqlConnection { PlanningRows = [PlanningRow()], CompteRenduRows = [CompteRenduRow()] }; var repository = new BatikCompteRenduRepository(connection); await repository.CreateAsync("42", Command(Guid.NewGuid().ToString("D"))); ExecutedCommand insert = Insert(connection); Assert.Equal(CollaborateurDuRendezVous, CrIdCollaborateur(insert)); } // ========================================================================= // Helpers // ========================================================================= private static CompteRenduCreateCommand Command(string? collaborateurId) => new() { CollaborateurId = collaborateurId, NoteCollaborateur = "Note du technicien" }; private static Dictionary PlanningRow() => new() { ["PL_PKID"] = RendezVousId, ["PL_IDIntervention"] = 12L, ["PL_IDCollaborateur"] = CollaborateurDuRendezVous, ["PL_IDGroupe"] = 0L }; private static Dictionary CompteRenduRow() => new() { ["CR_PKID"] = 1L, ["GUID"] = Guid.NewGuid().ToString("D") }; private static ExecutedCommand Insert(FakeHfsqlConnection connection) => Assert.Single(connection.Writes.Where(write => write.Sql.Contains("INSERT INTO CompteRendu", StringComparison.OrdinalIgnoreCase)).ToList()); /// Valeur du paramètre de la colonne CR_IDCollaborateur. private static object? CrIdCollaborateur(ExecutedCommand insert) => HfsqlInsertParameters.ValueFor(insert.Sql, insert.Parameters, "CR_IDCollaborateur"); private sealed record ExecutedCommand(string Sql, IReadOnlyList Parameters); private sealed class FakeHfsqlConnection : IHfsqlConnection { public HfsqlOptions Options => new(); public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed; private readonly List _commands = []; private readonly List _writes = []; /// Écritures (ExecuteNonQuery) dans l'ordre d'exécution. public IReadOnlyList Writes => _writes; /// Lignes retournées pour FROM Planning_RDV. public IReadOnlyList> PlanningRows { get; set; } = []; /// Lignes retournées pour FROM CompteRendu. public IReadOnlyList> CompteRenduRows { get; set; } = []; public Task OpenAsync(CancellationToken cancellationToken = default) { State = HfsqlConnectionState.Open; return Task.CompletedTask; } public Task CloseAsync() { State = HfsqlConnectionState.Closed; return Task.CompletedTask; } public Task ExecuteScalarAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { _commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? [])); return Task.FromResult(null); } public Task ExecuteNonQueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { var command = new ExecutedCommand(sql, parameters?.ToArray() ?? []); _commands.Add(command); _writes.Add(command); return Task.FromResult(0); } public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { _commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? [])); return Task.FromResult(ResolveRows(sql)); } public Task> GetColumnsAsync( string tableName, CancellationToken cancellationToken = default) => Task.FromResult>([]); public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } private IReadOnlyList> ResolveRows(string sql) { if (ContainsFrom(sql, "Planning_RDV")) return PlanningRows; if (ContainsFrom(sql, "CompteRendu")) return CompteRenduRows; return []; } private static bool ContainsFrom(string sql, string table) { string marker = $"FROM {table}"; int index = sql.IndexOf(marker, StringComparison.OrdinalIgnoreCase); while (index >= 0) { int next = index + marker.Length; if (next >= sql.Length || (!char.IsLetterOrDigit(sql[next]) && sql[next] != '_')) { return true; } index = sql.IndexOf(marker, index + 1, StringComparison.OrdinalIgnoreCase); } return false; } } }