239 lines
8.9 KiB
C#
239 lines
8.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|