289 lines
12 KiB
C#
289 lines
12 KiB
C#
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Infrastructure;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Local.Services;
|
|
using Omika.Core.Contracts.Common;
|
|
using Omika.Core.Contracts.Security;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests de la préférence LOCALE « pause déjeuner » : identification métier du
|
|
/// type de temps, stockage SQLite par utilisateur, isolation entre utilisateurs
|
|
/// et valeurs par défaut. Aucun appel réseau n'est impliqué.
|
|
/// </summary>
|
|
public class PauseDejeunerPreferenceTests : LocalTestBase
|
|
{
|
|
// ===== Identification métier du type de temps « pause déjeuner =====
|
|
|
|
/// <summary>
|
|
/// L'identification privilégie la clé métier stable. Le repli sur le libellé
|
|
/// n'existe que pour la génération Batik (sans PA_CLE) et ne doit JAMAIS
|
|
/// confondre la pause déjeuner avec « Temps de pause », qui est un autre type.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("T.TEMPS.DJNER", true)] // clé métier Omika
|
|
[InlineData("t.temps.djner", true)] // insensible à la casse
|
|
[InlineData("Temps pause déjeuner", true)] // libellé Batik
|
|
[InlineData("Temps pause dejeuner", true)] // libellé sans accent
|
|
[InlineData("Pause déjeuner", true)] // libellé personnalisé
|
|
[InlineData("Temps de pause", false)] // type métier DIFFÉRENT
|
|
[InlineData("Temps de préparation", false)]
|
|
[InlineData("T.TEMPS.INTER", false)]
|
|
[InlineData("Temps d'intervention", false)]
|
|
[InlineData("Temps de trajet", false)]
|
|
[InlineData("T.TEMPS.GLB", false)]
|
|
[InlineData("T.TEMPS.TMPSNNFF", false)]
|
|
[InlineData("", false)]
|
|
[InlineData(null, false)]
|
|
public void IsPauseDejeunerTypeTemps_IdentifieLeTypeSansDependreDuLibelleExact(
|
|
string? identifier, bool attendu)
|
|
=> Assert.Equal(attendu, SuiviTempsConstants.IsPauseDejeunerTypeTemps(identifier));
|
|
|
|
[Fact]
|
|
public void FindPauseDejeunerTypeTemps_ParCleMetier_Omika()
|
|
{
|
|
IReadOnlyList<ReferenceValueDto> types =
|
|
[
|
|
new() { Key = "T.TEMPS.INTER", Label = "Temps d'intervention" },
|
|
new() { Key = SuiviTempsConstants.PauseDejeunerTypeTempsKey, Label = "Temps pause déjeuner" },
|
|
new() { Key = "T.TEMPS.TMPSNNFF", Label = "Temps non affecté" }
|
|
];
|
|
|
|
ReferenceValueDto? trouve = SuiviTempsConstants.FindPauseDejeunerTypeTemps(types);
|
|
|
|
Assert.NotNull(trouve);
|
|
Assert.Equal(SuiviTempsConstants.PauseDejeunerTypeTempsKey, trouve!.Key);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPauseDejeunerTypeTemps_ParLibelle_Batik()
|
|
{
|
|
// Génération Batik : aucune PA_CLE, le libellé tient lieu de clé.
|
|
IReadOnlyList<ReferenceValueDto> types =
|
|
[
|
|
new() { Label = "Temps d'intervention" },
|
|
new() { Label = "Temps pause déjeuner" },
|
|
new() { Label = "Temps de pause" }
|
|
];
|
|
|
|
ReferenceValueDto? trouve = SuiviTempsConstants.FindPauseDejeunerTypeTemps(types);
|
|
|
|
Assert.NotNull(trouve);
|
|
Assert.Equal("Temps pause déjeuner", trouve!.Label);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPauseDejeunerTypeTemps_SansPauseDejeuner_RetourneNull()
|
|
{
|
|
IReadOnlyList<ReferenceValueDto> types =
|
|
[
|
|
new() { Label = "Temps d'intervention" },
|
|
new() { Label = "Temps de pause" }
|
|
];
|
|
|
|
Assert.Null(SuiviTempsConstants.FindPauseDejeunerTypeTemps(types));
|
|
}
|
|
|
|
// ===== Repository : stockage local par utilisateur =====
|
|
|
|
[Fact]
|
|
public async Task Repository_AllerRetour_ConserveLaPrecisionAMinute()
|
|
{
|
|
await PreferencesUtilisateurRepository.SavePauseDejeunerAsync(
|
|
"U1", new TimeSpan(12, 15, 0), new TimeSpan(13, 15, 0));
|
|
|
|
PauseDejeunerPreference? relue =
|
|
await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("U1");
|
|
|
|
Assert.NotNull(relue);
|
|
Assert.Equal(new TimeSpan(12, 15, 0), relue!.Debut);
|
|
Assert.Equal(new TimeSpan(13, 15, 0), relue.Fin);
|
|
Assert.True(relue.EstValide);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_AucunePreference_RetourneNull()
|
|
=> Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("INCONNU"));
|
|
|
|
[Fact]
|
|
public async Task Repository_DeuxUtilisateurs_NeSeMelangentPas()
|
|
{
|
|
await PreferencesUtilisateurRepository.SavePauseDejeunerAsync(
|
|
"UTILISATEUR-A", new TimeSpan(12, 0, 0), new TimeSpan(13, 0, 0));
|
|
await PreferencesUtilisateurRepository.SavePauseDejeunerAsync(
|
|
"UTILISATEUR-B", new TimeSpan(12, 30, 0), new TimeSpan(13, 30, 0));
|
|
|
|
PauseDejeunerPreference? a = await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("UTILISATEUR-A");
|
|
PauseDejeunerPreference? b = await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("UTILISATEUR-B");
|
|
|
|
Assert.Equal(new TimeSpan(12, 0, 0), a!.Debut);
|
|
Assert.Equal(new TimeSpan(13, 0, 0), a.Fin);
|
|
Assert.Equal(new TimeSpan(12, 30, 0), b!.Debut);
|
|
Assert.Equal(new TimeSpan(13, 30, 0), b.Fin);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_EnregistrementDeuxFois_MetAJourLaMemeLigne()
|
|
{
|
|
await PreferencesUtilisateurRepository.SavePauseDejeunerAsync(
|
|
"U1", new TimeSpan(12, 0, 0), new TimeSpan(13, 0, 0));
|
|
await PreferencesUtilisateurRepository.SavePauseDejeunerAsync(
|
|
"U1", new TimeSpan(11, 45, 0), new TimeSpan(12, 45, 0));
|
|
|
|
object? nombre = await Database.ExecuteScalarAsync(
|
|
"SELECT COUNT(*) FROM \"PreferencesUtilisateur\" WHERE \"UtilisateurId\" = 'U1'");
|
|
Assert.Equal(1L, Convert.ToInt64(nombre));
|
|
|
|
PauseDejeunerPreference? relue = await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("U1");
|
|
Assert.Equal(new TimeSpan(11, 45, 0), relue!.Debut);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_ValeurIncoherenteEnBase_EstIgnoreeSansErreur()
|
|
{
|
|
// Fin ANTÉRIEURE au début : une valeur corrompue ne doit jamais produire
|
|
// une pause aberrante, ni faire échouer la lecture.
|
|
await Database.ExecuteNonQueryAsync(
|
|
"INSERT INTO \"PreferencesUtilisateur\" "
|
|
+ "(\"UtilisateurId\", \"PauseDejeunerDebut\", \"PauseDejeunerFin\", \"UpdatedAtUtc\") "
|
|
+ "VALUES ('U1', '13:00', '12:00', '2026-01-01T00:00:00Z')");
|
|
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("U1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_ValeurIllisible_EstIgnoreeSansErreur()
|
|
{
|
|
await Database.ExecuteNonQueryAsync(
|
|
"INSERT INTO \"PreferencesUtilisateur\" "
|
|
+ "(\"UtilisateurId\", \"PauseDejeunerDebut\", \"PauseDejeunerFin\", \"UpdatedAtUtc\") "
|
|
+ "VALUES ('U1', 'midi', '13h00', '2026-01-01T00:00:00Z')");
|
|
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("U1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_HeuresNulles_EstIgnoreSansErreur()
|
|
{
|
|
await Database.ExecuteNonQueryAsync(
|
|
"INSERT INTO \"PreferencesUtilisateur\" "
|
|
+ "(\"UtilisateurId\", \"PauseDejeunerDebut\", \"PauseDejeunerFin\", \"UpdatedAtUtc\") "
|
|
+ "VALUES ('U1', NULL, NULL, '2026-01-01T00:00:00Z')");
|
|
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync("U1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repository_IdentifiantVide_NeLevePasEtRetourneNull()
|
|
=> Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(" "));
|
|
|
|
// ===== Service : identité de l'utilisateur connecté =====
|
|
|
|
[Fact]
|
|
public async Task Service_SansPreferenceEnregistree_RetourneLesValeursParDefautSansEcrire()
|
|
{
|
|
var service = CreateService();
|
|
|
|
PauseDejeunerPreference preference = await service.GetAsync();
|
|
|
|
Assert.Equal(PauseDejeunerPreference.DebutParDefaut, preference.Debut);
|
|
Assert.Equal(PauseDejeunerPreference.FinParDefaut, preference.Fin);
|
|
|
|
// La lecture seule ne crée aucune préférence.
|
|
object? nombre = await Database.ExecuteScalarAsync(
|
|
"SELECT COUNT(*) FROM \"PreferencesUtilisateur\"");
|
|
Assert.Equal(0L, Convert.ToInt64(nombre));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Service_UtilisateurA_PuisUtilisateurB_ChacunRetrouveSesHoraires()
|
|
{
|
|
// Utilisateur A configure 12:00 → 13:00.
|
|
Connecte("Utilisateur-A");
|
|
var serviceA = CreateService();
|
|
await serviceA.SaveAsync(new TimeSpan(12, 0, 0), new TimeSpan(13, 0, 0));
|
|
|
|
// Déconnexion / reconnexion : utilisateur B configure 12:30 → 13:30.
|
|
Connecte("Utilisateur-B");
|
|
var serviceB = CreateService();
|
|
await serviceB.SaveAsync(new TimeSpan(12, 30, 0), new TimeSpan(13, 30, 0));
|
|
|
|
// B retrouve ses horaires.
|
|
PauseDejeunerPreference pourB = await serviceB.GetAsync();
|
|
Assert.Equal(new TimeSpan(12, 30, 0), pourB.Debut);
|
|
Assert.Equal(new TimeSpan(13, 30, 0), pourB.Fin);
|
|
|
|
// A retrouve les siens, intacts.
|
|
Connecte("Utilisateur-A");
|
|
PauseDejeunerPreference pourA = await CreateService().GetAsync();
|
|
Assert.Equal(new TimeSpan(12, 0, 0), pourA.Debut);
|
|
Assert.Equal(new TimeSpan(13, 0, 0), pourA.Fin);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Service_PreferencePersistante_SurvitAUneNouvelleInstance()
|
|
{
|
|
Connecte("Utilisateur-A");
|
|
await CreateService().SaveAsync(new TimeSpan(12, 15, 0), new TimeSpan(13, 15, 0));
|
|
|
|
// Simule un redémarrage complet : nouvelle instance de service sur la
|
|
// MÊME base locale (aucun réseau, aucune synchronisation).
|
|
PauseDejeunerPreference relue = await CreateService().GetAsync();
|
|
|
|
Assert.Equal(new TimeSpan(12, 15, 0), relue.Debut);
|
|
Assert.Equal(new TimeSpan(13, 15, 0), relue.Fin);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Service_FinAnterieureOuEgaleAuDebut_RefuseLEnregistrement()
|
|
{
|
|
var service = CreateService();
|
|
|
|
await Assert.ThrowsAsync<ArgumentException>(
|
|
() => service.SaveAsync(new TimeSpan(13, 30, 0), new TimeSpan(12, 0, 0)));
|
|
await Assert.ThrowsAsync<ArgumentException>(
|
|
() => service.SaveAsync(new TimeSpan(12, 0, 0), new TimeSpan(12, 0, 0)));
|
|
|
|
// Aucune configuration incohérente n'a été enregistrée.
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Service_SansBootstrap_UtiliseLaCleDeRepliSansLeve()
|
|
{
|
|
SessionService.Bootstrap = null;
|
|
var service = CreateService();
|
|
|
|
await service.SaveAsync(new TimeSpan(12, 0, 0), new TimeSpan(13, 0, 0));
|
|
PauseDejeunerPreference relue = await service.GetAsync();
|
|
|
|
Assert.Equal(new TimeSpan(12, 0, 0), relue.Debut);
|
|
Assert.NotNull(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Service_BootstrapSansUtilisateur_UtiliseLaCleDeRepli()
|
|
{
|
|
SessionService.Bootstrap = new LocalBootstrapState { Utilisateur = null };
|
|
var service = CreateService();
|
|
|
|
await service.SaveAsync(new TimeSpan(12, 30, 0), new TimeSpan(13, 30, 0));
|
|
|
|
Assert.NotNull(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur));
|
|
}
|
|
|
|
/// <summary>Branche un utilisateur connecté dans le bootstrap local.</summary>
|
|
private void Connecte(string utilisateurId)
|
|
=> SessionService.Bootstrap = new LocalBootstrapState
|
|
{
|
|
Utilisateur = new CurrentUserDto { Id = utilisateurId, Login = utilisateurId }
|
|
};
|
|
|
|
private IPauseDejeunerPreferenceService CreateService()
|
|
=> new PauseDejeunerPreferenceService(PreferencesUtilisateurRepository, SessionService);
|
|
}
|