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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,238 @@
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;
/// <summary>
/// Tests unitaires de la création d'un compte rendu Batik
/// (<see cref="BatikCompteRenduRepository.CreateAsync"/>) centrés sur la colonne
/// <c>CR_IDCollaborateur</c> : aucun serveur HFSQL, aucun ODBC réel, aucun réseau.
/// Un fake <see cref="IHfsqlConnection"/> enregistre le SQL et les paramètres, et
/// retourne des dictionnaires selon la table interrogée.
///
/// Objectifs :
/// <list type="bullet">
/// <item>la colonne porte l'identité du collaborateur AUTEUR du compte rendu, et
/// non plus systématiquement le collaborateur affecté à <c>Planning_RDV</c> ;</item>
/// <item>le PK numérique du jeton mobile est repris tel quel, et le GUID
/// synthétique réversible Batik est décodé (la table <c>Collaborateur</c> Batik
/// ne porte aucune colonne GUID) ;</item>
/// <item>à défaut d'auteur résolvable, le collaborateur affecté au rendez-vous
/// reste le repli historique : la colonne n'est jamais laissée vide.</item>
/// </list>
/// </summary>
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<string, object?> PlanningRow()
=> new()
{
["PL_PKID"] = RendezVousId,
["PL_IDIntervention"] = 12L,
["PL_IDCollaborateur"] = CollaborateurDuRendezVous,
["PL_IDGroupe"] = 0L
};
private static Dictionary<string, object?> 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());
/// <summary>Valeur du paramètre de la colonne <c>CR_IDCollaborateur</c>.</summary>
private static object? CrIdCollaborateur(ExecutedCommand insert)
=> HfsqlInsertParameters.ValueFor(insert.Sql, insert.Parameters, "CR_IDCollaborateur");
private sealed record ExecutedCommand(string Sql, IReadOnlyList<object?> Parameters);
private sealed class FakeHfsqlConnection : IHfsqlConnection
{
public HfsqlOptions Options => new();
public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed;
private readonly List<ExecutedCommand> _commands = [];
private readonly List<ExecutedCommand> _writes = [];
/// <summary>Écritures (ExecuteNonQuery) dans l'ordre d'exécution.</summary>
public IReadOnlyList<ExecutedCommand> Writes => _writes;
/// <summary>Lignes retournées pour <c>FROM Planning_RDV</c>.</summary>
public IReadOnlyList<IReadOnlyDictionary<string, object?>> PlanningRows { get; set; } = [];
/// <summary>Lignes retournées pour <c>FROM CompteRendu</c>.</summary>
public IReadOnlyList<IReadOnlyDictionary<string, object?>> 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<object?> ExecuteScalarAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
_commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? []));
return Task.FromResult<object?>(null);
}
public Task<int> ExecuteNonQueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
var command = new ExecutedCommand(sql, parameters?.ToArray() ?? []);
_commands.Add(command);
_writes.Add(command);
return Task.FromResult(0);
}
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
_commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? []));
return Task.FromResult(ResolveRows(sql));
}
public Task<IReadOnlyList<HfsqlColumnMetadata>> GetColumnsAsync(
string tableName,
CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<HfsqlColumnMetadata>>([]);
public ValueTask DisposeAsync()
{
State = HfsqlConnectionState.Closed;
return ValueTask.CompletedTask;
}
private IReadOnlyList<IReadOnlyDictionary<string, object?>> 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;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
using Omika.Core.Contracts.Mobile.Bootstrap;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.Repositories.Batik;
namespace Omika.Tests.Repositories.Batik;
/// <summary>
/// Tests du repository d'identité mobile Batik.
/// Vérifie que le chemin historique Batik continue de résoudre les permissions
/// (UT_IDTypeProfil → AccesEtDroit.AED_IDProfil + modules ParametreMetier) et
/// que l'utilisateur est désormais résolu par UT_PKID (et non par UT_Login).
/// </summary>
public class BatikMobileIdentityRepositoryTests
{
private const long UserIdTest = 1001;
private const long ProfilIdTest = 3001;
private const long ModulePkTest = 5001;
private const string LoginTest = "TEST_USER";
[Fact]
public async Task GetPermissionsAsync_ResoutParUT_PKID_EtRetourneALicMob()
{
var connection = new FakeHfsqlConnection(HfsqlConnectionState.Open);
connection.UtilisateurRows.Add(UtilisateurRow(UserIdTest, LoginTest, ProfilIdTest));
connection.ParametreMetierRows.Add(ModuleRow(ModulePkTest, "M.PlClb.11")); // → A.LIC.MOB
connection.AccesEtDroitRows.Add(DroitRow(ProfilIdTest, ModulePkTest, true));
var repository = new BatikMobileIdentityRepository(connection);
IReadOnlyDictionary<string, bool> permissions =
await repository.GetPermissionsAsync(UserIdTest.ToString());
Assert.True(permissions["A.LIC.MOB"]);
string utilisateurSql = connection.Queries.First(q => q.Contains("FROM Utilisateur", StringComparison.OrdinalIgnoreCase));
Assert.Contains("UT_PKID = ?", utilisateurSql);
Assert.DoesNotContain("UT_Login = ?", utilisateurSql);
}
[Fact]
public async Task GetLoginByUserIdAsync_RetourneLoginReel()
{
var connection = new FakeHfsqlConnection(HfsqlConnectionState.Open);
connection.UtilisateurRows.Add(UtilisateurRow(UserIdTest, LoginTest, ProfilIdTest));
var repository = new BatikMobileIdentityRepository(connection);
string? login = await repository.GetLoginByUserIdAsync(UserIdTest.ToString());
Assert.Equal(LoginTest, login);
}
[Fact]
public async Task GetProfileAsync_ResoutProfilBatik()
{
var connection = new FakeHfsqlConnection(HfsqlConnectionState.Open);
connection.UtilisateurRows.Add(UtilisateurRow(UserIdTest, LoginTest, ProfilIdTest));
connection.ParametreMetierRows.Add(ProfilRow(ProfilIdTest, "BATIK_PROFIL"));
var repository = new BatikMobileIdentityRepository(connection);
MobileProfileDto profile = await repository.GetProfileAsync(UserIdTest.ToString());
Assert.Equal("PROFIL", profile.Code);
Assert.Equal("BATIK_PROFIL", profile.Nom);
}
// ===== Fixtures =====
private static Dictionary<string, object?> UtilisateurRow(long pkId, string login, long profilId) => new()
{
["UT_PKID"] = pkId,
["UT_Login"] = login,
["UT_IDCollaborateur"] = 2001L,
["UT_IDTypeProfil"] = profilId,
["UT_AccesMobile"] = true
};
// Module de la catégorie 29 (« Modules intervenir ») : PA_Valeur = clé Batik.
private static Dictionary<string, object?> ModuleRow(long pkId, string valeur) => new()
{
["PA_PKID"] = pkId,
["PA_Code"] = "29",
["PA_Libelle"] = "Module",
["PA_Valeur"] = valeur,
["PA_Actif"] = true
};
private static Dictionary<string, object?> ProfilRow(long pkId, string libelle) => new()
{
["PA_PKID"] = pkId,
["PA_Code"] = "PROFIL",
["PA_Libelle"] = libelle,
["PA_Valeur"] = libelle,
["PA_Actif"] = true
};
private static Dictionary<string, object?> DroitRow(long profilId, long moduleId, bool acces) => new()
{
["AED_PKID"] = 4001L,
["AED_IDProfil"] = profilId,
["AED_IDModule"] = moduleId,
["AED_Acces"] = acces
};
// ===== Fake =====
private sealed class FakeHfsqlConnection : IHfsqlConnection
{
public FakeHfsqlConnection(HfsqlConnectionState state) => State = state;
public HfsqlOptions Options => new();
public HfsqlConnectionState State { get; private set; }
public List<Dictionary<string, object?>> UtilisateurRows { get; } = [];
public List<Dictionary<string, object?>> ParametreMetierRows { get; } = [];
public List<Dictionary<string, object?>> AccesEtDroitRows { get; } = [];
public List<string> Queries { get; } = [];
public Task OpenAsync(CancellationToken cancellationToken = default)
{
State = HfsqlConnectionState.Open;
return Task.CompletedTask;
}
public Task CloseAsync()
{
State = HfsqlConnectionState.Closed;
return Task.CompletedTask;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public Task<object?> ExecuteScalarAsync(string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>(null);
public Task<int> ExecuteNonQueryAsync(string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
=> Task.FromResult(0);
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
{
Queries.Add(sql);
IReadOnlyList<IReadOnlyDictionary<string, object?>> result = sql switch
{
_ when sql.Contains("FROM Utilisateur", StringComparison.OrdinalIgnoreCase) => UtilisateurRows,
_ when sql.Contains("FROM ParametreMetier", StringComparison.OrdinalIgnoreCase) => ParametreMetierRows,
_ when sql.Contains("FROM AccesEtDroit", StringComparison.OrdinalIgnoreCase) => AccesEtDroitRows,
_ => []
};
return Task.FromResult(result);
}
public Task<IReadOnlyList<HfsqlColumnMetadata>> GetColumnsAsync(string tableName, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<HfsqlColumnMetadata>>([]);
}
}
@@ -0,0 +1,328 @@
using Omika.Core.Domain;
using Omika.Core.Domain.Commands;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.HfSql.Metadata;
using Omika.Data.Repositories.Batik;
namespace Omika.Tests.Repositories.Batik;
/// <summary>
/// Tests unitaires de l'annulation d'un rendez-vous Batik
/// (<see cref="BatikRendezVousRepository.CancelAsync"/>) : aucun serveur HFSQL,
/// aucun ODBC réel, aucun réseau. Un fake <see cref="IHfsqlConnection"/> enregistre
/// le SQL et les paramètres et retourne des dictionnaires selon la table interrogée.
///
/// Objectifs :
/// <list type="bullet">
/// <item>un rendez-vous INDIVIDUEL (<c>PL_IDGroupe = 0</c>) n'annule que sa propre
/// ligne <c>Planning_RDV</c> (ciblée par <c>PL_PKID</c>) ;</item>
/// <item>un rendez-vous GROUPÉ (<c>PL_IDGroupe &gt; 0</c>) annule TOUTES les lignes
/// du groupe (ciblées par <c>PL_IDGroupe</c>) : tous les rendez-vous du groupe sont
/// impactés ;</item>
/// <item>la description reçoit la mention d'annulation
/// <c>Note (Nom) : message</c>, le nom étant résolu côté serveur s'il n'est pas
/// fourni.</item>
/// </list>
/// </summary>
public class BatikRendezVousAnnulationTests
{
private const long RendezVousId = 42L;
private const string DescriptionInitiale = "Description initiale";
[Fact]
public async Task CancelAsync_RdvIndividuel_CibleUniquementSaPropreLigne()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Contains("UPDATE Planning_RDV SET", write.Sql);
// Aucun groupe : la ligne est ciblée par sa clé primaire.
Assert.Contains("WHERE PL_PKID = ?", write.Sql);
Assert.DoesNotContain("PL_IDGroupe", write.Sql);
// Statut « Annulé » (ParametreMetier PA_PKID=1000) + origine + motif.
Assert.Equal(1000L, write.Parameters[2]);
Assert.Equal(2000L, write.Parameters[0]);
Assert.Equal(3000L, write.Parameters[1]);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
// L'annulation modifie la ligne : elle porte l'estampille d'audit. Sans
// auteur résolvable, ModifDate seul est écrit (jamais ModifUser = 0).
Assert.Contains("ModifDate = ?", write.Sql);
Assert.DoesNotContain("ModifUser", write.Sql);
Assert.IsType<DateTime>(write.Parameters[4]);
Assert.Equal(RendezVousId, write.Parameters[5]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_RdvGroupe_AnnuleToutesLesLignesDuGroupe()
{
var connection = new FakeHfsqlConnection
{
// Le rendez-vous appartient au groupe 99 : plusieurs lignes Planning_RDV
// partagent ce PL_IDGroupe (une par collaborateur).
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 99L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
// Le groupe entier est annulé : ciblage par PL_IDGroupe, jamais par PL_PKID.
Assert.Contains("WHERE PL_IDGroupe = ?", write.Sql);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
// Estampille d'audit (ModifDate) puis la clé du groupe.
Assert.Contains("ModifDate = ?", write.Sql);
Assert.Equal(99L, write.Parameters[5]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_AvecAuteur_EstampilleModifDateEtModifUser()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync(
"42",
new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
},
auteur: new AuditActor("77", null));
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Contains("ModifDate = ?", write.Sql);
Assert.Contains("ModifUser = ?", write.Sql);
Assert.IsType<DateTime>(write.Parameters[4]);
Assert.Equal(77L, write.Parameters[5]);
Assert.Equal(RendezVousId, write.Parameters[6]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_SansNomCollaborateur_ResoutLeNomDepuisLeCollaborateurAffecte()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows(),
CollaborateurRows =
[
new Dictionary<string, object?>
{
["CO_PKID"] = 7L,
["CO_Nom"] = "DUPONT",
["CO_Prenom"] = "Jean"
}
]
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
}
// =========================================================================
// Helpers
// =========================================================================
/// <summary>
/// Vérifie que le nombre de paramètres positionnels correspond EXACTEMENT au
/// nombre de <c>?</c> du SQL : une estampille d'audit oubliée (ou en trop)
/// décale les paramètres et corrompt silencieusement l'écriture.
/// </summary>
private static void AssertParametersMatchPlaceholders(ExecutedCommand command)
{
int placeholders = command.Sql.Count(c => c == '?');
Assert.Equal(placeholders, command.Parameters.Count);
}
private static Dictionary<string, object?> PlanningRow(long pkid, long idGroupe, long collaborateurId)
=> new()
{
["PL_PKID"] = pkid,
["PL_Description"] = DescriptionInitiale,
["PL_IDGroupe"] = idGroupe,
["PL_IDCollaborateur"] = collaborateurId
};
/// <summary>Référentiel minimal : origine, motif et statut « Annulé ».</summary>
private static IReadOnlyList<IReadOnlyDictionary<string, object?>> ReferenceRows() =>
[
new Dictionary<string, object?>
{
["PA_PKID"] = 2000L,
["PA_Code"] = "O1",
["PA_Libelle"] = "Externe : Client"
},
new Dictionary<string, object?>
{
["PA_PKID"] = 3000L,
["PA_Code"] = "M1",
["PA_Libelle"] = "Absent"
},
new Dictionary<string, object?>
{
["PA_PKID"] = 1000L,
["PA_Code"] = "15",
["PA_Valeur"] = "S.RDV.ANNU",
["PA_Libelle"] = "Annulé"
}
];
private sealed record ExecutedCommand(string Sql, IReadOnlyList<object?> Parameters);
private sealed class FakeHfsqlConnection : IHfsqlConnection
{
public HfsqlOptions Options => new();
public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed;
private readonly List<ExecutedCommand> _commands = [];
private readonly List<ExecutedCommand> _writes = [];
/// <summary>Écritures (ExecuteNonQuery) dans l'ordre d'exécution.</summary>
public IReadOnlyList<ExecutedCommand> Writes => _writes;
/// <summary>Lignes retournées pour <c>FROM Planning_RDV</c>.</summary>
public IReadOnlyList<IReadOnlyDictionary<string, object?>> PlanningRows { get; set; } = [];
/// <summary>Lignes retournées pour <c>FROM ParametreMetier</c>.</summary>
public IReadOnlyList<IReadOnlyDictionary<string, object?>> ParametreMetierRows { get; set; } = [];
/// <summary>Lignes retournées pour <c>FROM Collaborateur</c>.</summary>
public IReadOnlyList<IReadOnlyDictionary<string, object?>> CollaborateurRows { 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<object?> ExecuteScalarAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
_commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? []));
return Task.FromResult<object?>(null);
}
public Task<int> ExecuteNonQueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
var command = new ExecutedCommand(sql, parameters?.ToArray() ?? []);
_commands.Add(command);
_writes.Add(command);
return Task.FromResult(0);
}
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
_commands.Add(new ExecutedCommand(sql, parameters?.ToArray() ?? []));
return Task.FromResult(ResolveRows(sql));
}
public Task<IReadOnlyList<HfsqlColumnMetadata>> GetColumnsAsync(
string tableName,
CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<HfsqlColumnMetadata>>([]);
public ValueTask DisposeAsync()
{
State = HfsqlConnectionState.Closed;
return ValueTask.CompletedTask;
}
private IReadOnlyList<IReadOnlyDictionary<string, object?>> ResolveRows(string sql)
{
if (ContainsFrom(sql, "Planning_RDV"))
return PlanningRows;
if (ContainsFrom(sql, "ParametreMetier"))
return ParametreMetierRows;
if (ContainsFrom(sql, "Collaborateur"))
return CollaborateurRows;
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;
}
}
}