Premiere commit - Omika
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user