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
@@ -0,0 +1,85 @@
using Omika.Core.Contracts.Clients;
using Omika.Core.Contracts.Common;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests des contrats Clients : séparation liste / détail / création /
/// modification et absence de champs physiques.
/// </summary>
public class ClientContractTests
{
[Fact]
public void ClientSummaryDto_ContainsOnlyListFields()
{
var summary = new ClientSummaryDto
{
Id = "CL-001",
Code = "C001",
Nom = "Ville de Lyon",
Type = "Collectivité",
Statut = "Actif",
Ville = "Lyon",
Telephone = "04 00 00 00 00",
EstBloque = false,
EstArchive = false
};
Assert.Equal("CL-001", summary.Id);
Assert.Equal("Ville de Lyon", summary.Nom);
Assert.False(summary.EstArchive);
}
[Fact]
public void ClientDetailsDto_CanHoldCollections()
{
var details = new ClientDetailsDto
{
Id = "CL-001",
Nom = "Ville de Lyon",
Adresses = [new AdresseDto { Id = "AD-1", Etiquette = "Mairie" }],
Contacts = [new ContactDto { Id = "CT-1", Nom = "Dupont", Prenom = "Jean" }],
Sites = [new SiteDto { Id = "ST-1", Nom = "Hôtel de ville" }]
};
Assert.Single(details.Adresses);
Assert.Single(details.Contacts);
Assert.Single(details.Sites);
Assert.Equal("Hôtel de ville", details.Sites[0].Nom);
}
[Fact]
public void CreateClientRequest_DoesNotAcceptPhysicalIdOrAuditFields()
{
var request = new CreateClientRequest { Nom = "Client test" };
Assert.Null(request.AdressePrincipale);
Assert.Null(request.Coordonnees);
// Aucune propriété d'audit ou identifiant physique ne doit être acceptée.
foreach (var property in typeof(CreateClientRequest).GetProperties())
{
Assert.DoesNotContain("Pkid", property.Name, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("DateCreation", property.Name, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("ModifiePar", property.Name, StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void UpdateClientRequest_AllowsOptionalBloquage()
{
var request = new UpdateClientRequest { Nom = "Renommé", EstBloque = true };
Assert.True(request.EstBloque);
Assert.Equal("Renommé", request.Nom);
}
[Fact]
public void SearchClientsRequest_HasNoPhysicalPrefix()
{
var request = new SearchClientsRequest { Nom = "Lyon", Statut = "Actif" };
Assert.Equal("Lyon", request.Nom);
Assert.Equal("Actif", request.Statut);
}
}
@@ -0,0 +1,124 @@
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Organisation;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests des contrats Organisation (collaborateurs) et de leur neutralité totale
/// vis-à-vis des tables physiques HFSQL.
/// </summary>
public class CollaborateurContractTests
{
[Fact]
public void CollaborateurDetailsDto_IsFreeOfPhysicalNames()
{
var dto = new CollaborateurDetailsDto
{
Id = "COL-001",
Nom = "Dupont",
Prenom = "Jean"
};
string[] forbidden =
[
"CL_PKID", "PKID", "CL_Nom", "CL_Prenom"
];
foreach (string name in forbidden)
{
Assert.DoesNotContain(name, dto.GetType().Name);
Assert.DoesNotContain(name, dto.Nom);
}
}
[Fact]
public void CollaborateurDetailsDto_CanCarryCoordonnees()
{
var dto = new CollaborateurDetailsDto
{
Id = "COL-001",
Coordonnees = new ContactDetailsDto { Email = "jean@example.com", Phone = "0102030405" }
};
Assert.Equal("jean@example.com", dto.Coordonnees.Email);
Assert.Equal("0102030405", dto.Coordonnees.Phone);
}
[Fact]
public void CollaborateurDetailsDto_CanCarryReferences()
{
var dto = new CollaborateurDetailsDto
{
Id = "COL-001",
Civilite = new ReferenceValueDto { Key = "M", Label = "M." },
Statut = new ReferenceValueDto { Key = "ACTIF", Label = "Actif" }
};
Assert.Equal("M.", dto.Civilite.Label);
Assert.Equal("Actif", dto.Statut.Label);
}
[Fact]
public void CollaborateurSummaryDto_IsFreeOfPhysicalNames()
{
var dto = new CollaborateurSummaryDto
{
Id = "COL-001",
NomAffiche = "Jean Dupont"
};
string[] forbidden =
[
"CL_PKID", "PKID", "CL_Nom", "CL_Prenom"
];
foreach (string name in forbidden)
{
Assert.DoesNotContain(name, dto.GetType().Name);
Assert.DoesNotContain(name, dto.NomAffiche);
}
}
[Fact]
public void CreateCollaborateurRequest_IsIdenticalForBothSchemas()
{
// Le même contrat sert pour l'ancien et le nouveau schéma.
var request = new CreateCollaborateurRequest
{
Code = "JD",
Nom = "Dupont",
Prenom = "Jean"
};
Assert.Equal("JD", request.Code);
Assert.Equal("Dupont", request.Nom);
}
[Fact]
public void UpdateCollaborateurRequest_IsIdenticalForBothSchemas()
{
var request = new UpdateCollaborateurRequest
{
Nom = "Martin",
Actif = false
};
Assert.Equal("Martin", request.Nom);
Assert.False(request.Actif);
}
[Fact]
public void SearchCollaborateurRequest_IsIdenticalForBothSchemas()
{
var request = new SearchCollaborateurRequest
{
Search = "Dupont",
EquipeId = "equipe-1",
Actif = true
};
Assert.Equal("Dupont", request.Search);
Assert.Equal("equipe-1", request.EquipeId);
Assert.True(request.Actif);
}
}
@@ -0,0 +1,81 @@
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Interventions;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests des contrats Interventions (résumé, détail, création, modification,
/// recherche) et de leur neutralité vis-à-vis du stockage.
/// </summary>
public class InterventionContractTests
{
[Fact]
public void InterventionSummaryDto_ContainsOnlyListFields()
{
var summary = new InterventionSummaryDto
{
Id = "IN-001",
Reference = "2026-0001",
Titre = "Dépannage chaudière",
Etat = "En cours",
Type = "Maintenance",
DatePrevueDebut = new DateTime(2026, 1, 15, 8, 0, 0)
};
Assert.Equal("2026-0001", summary.Reference);
Assert.Equal("Maintenance", summary.Type);
}
[Fact]
public void InterventionDetailsDto_HoldsActionsCompteRendusAndReferences()
{
var details = new InterventionDetailsDto
{
Id = "IN-001",
Etat = new ReferenceValueDto { Label = "En cours" },
CollaborateurIds = ["COL-1", "COL-2"],
Actions = [new ActionInterventionDto { Description = "Commande pièce", Terminee = true }],
ComptesRendus = [new CompteRenduDto { Contenu = "Chaudière relancée" }]
};
Assert.Equal(2, details.CollaborateurIds.Count);
Assert.Single(details.Actions);
Assert.True(details.Actions[0].Terminee);
Assert.Equal("Chaudière relancée", details.ComptesRendus[0].Contenu);
}
[Fact]
public void CreateInterventionRequest_DoesNotAcceptPhysicalFields()
{
var request = new CreateInterventionRequest { Titre = "Intervention", Reference = "2026-0002" };
foreach (var property in typeof(CreateInterventionRequest).GetProperties())
{
Assert.DoesNotContain("Pkid", property.Name, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("HfSql", property.Name, StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void UpdateInterventionRequest_AcceptsEtatAndDates()
{
var request = new UpdateInterventionRequest
{
Titre = "Renommage",
Etat = new ReferenceValueDto { Label = "Terminé" },
DateReelleFin = new DateTime(2026, 1, 16, 12, 0, 0)
};
Assert.Equal("Terminé", request.Etat.Label);
Assert.NotNull(request.DateReelleFin);
}
[Fact]
public void SearchInterventionsRequest_HasNoPhysicalPrefix()
{
var request = new SearchInterventionsRequest { Titre = "chaudière", CollaborateurId = "COL-1" };
Assert.Equal("chaudière", request.Titre);
Assert.Equal("COL-1", request.CollaborateurId);
}
}
@@ -0,0 +1,80 @@
using Omika.Core.Contracts.Mobile.License;
using Omika.Core.Observability.Diagnostic;
namespace Omika.Tests.Contracts.License;
/// <summary>
/// Tests de l'application du snapshot d'appareil à la requête d'activation (T17-C).
/// Vérifie que OsType/OsVersion/AppVersion/DeviceInfo sont remplis depuis la MÊME
/// source que le diagnostic, sans jamais exposer de DeviceId ni de secret.
/// </summary>
public class LicenseActivateRequestDeviceExtensionsTests
{
[Fact]
public void ApplyDeviceSnapshot_FillsOsVersionAppAndDeviceInfo()
{
var request = new LicenseActivateRequest
{
CodeClient = "C",
NumeroLicence = "L",
DeviceId = "D"
};
var device = new DeviceInfoSnapshot
{
OperatingSystem = "Android",
OperatingSystemVersion = "16",
AppVersion = "1.0.0",
DeviceManufacturer = "Xiaomi",
DeviceModel = "23113RKC6G"
};
LicenseActivateRequest result = request.ApplyDeviceSnapshot(device);
Assert.Equal("Android", result.OsType);
Assert.Equal("16", result.OsVersion);
Assert.Equal("1.0.0", result.AppVersion);
Assert.Equal("manufacturer=Xiaomi;model=23113RKC6G", result.DeviceInfo);
Assert.Null(result.NotificationKey);
Assert.Equal("D", result.DeviceId);
}
[Fact]
public void ApplyDeviceSnapshot_NullLeavesFieldsNull()
{
var request = new LicenseActivateRequest
{
CodeClient = "C",
NumeroLicence = "L",
DeviceId = "D"
};
LicenseActivateRequest result = request.ApplyDeviceSnapshot(null);
Assert.Null(result.OsType);
Assert.Null(result.OsVersion);
Assert.Null(result.AppVersion);
Assert.Null(result.DeviceInfo);
Assert.Null(result.NotificationKey);
}
[Fact]
public void ApplyDeviceSnapshot_NeverExposesDeviceIdInDeviceInfo()
{
var request = new LicenseActivateRequest
{
CodeClient = "C",
NumeroLicence = "L",
DeviceId = "secret-device-id"
};
var device = new DeviceInfoSnapshot
{
DeviceManufacturer = "Apple",
DeviceModel = "iPhone17,2"
};
LicenseActivateRequest result = request.ApplyDeviceSnapshot(device);
Assert.DoesNotContain("secret-device-id", result.DeviceInfo);
Assert.DoesNotContain("device-id", result.DeviceInfo);
}
}
@@ -0,0 +1,55 @@
using Omika.Core.Contracts.Common;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests du contrat de pagination : bornes, calcul du nombre total de pages et
/// indicateurs de navigation.
/// </summary>
public class PagedResultTests
{
[Fact]
public void PagedRequest_NormalizesPage_ToAtLeastOne()
{
var request = new PagedRequest { Page = 0, PageSize = 10 };
Assert.Equal(1, request.Page);
}
[Fact]
public void PagedRequest_ClampsPageSize()
{
var request = new PagedRequest { PageSize = PagedRequest.MaxPageSize + 1 };
Assert.Equal(PagedRequest.MaxPageSize, request.PageSize);
}
[Fact]
public void PagedRequest_ZeroPageSize_FallsBackToDefault()
{
var request = new PagedRequest { PageSize = 0 };
Assert.Equal(20, request.PageSize);
}
[Fact]
public void PagedResult_ComputesTotalPages_AndNavigation()
{
var items = new[] { "a", "b", "c", "d", "e" };
var result = new PagedResult<string>(items, Page: 1, PageSize: 2, TotalCount: 5, TotalPages: 3);
Assert.Equal(5, result.TotalCount);
Assert.Equal(3, result.TotalPages);
Assert.False(result.HasPreviousPage);
Assert.True(result.HasNextPage);
}
[Fact]
public void PagedResult_LastPage_HasNoNextPage()
{
var result = new PagedResult<string>([], Page: 2, PageSize: 2, TotalCount: 4, TotalPages: 2);
Assert.True(result.HasPreviousPage);
Assert.False(result.HasNextPage);
}
}
@@ -0,0 +1,70 @@
using Omika.Core.Contracts.Configuration;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests du parseur de paramètres métier, centrés sur la conversion des
/// couleurs WinDev (<c>sCouleur</c>, entier <c>0xRRGGBB</c> sérialisé en
/// décimal) vers le format hexadécimal <c>#RRGGBB</c> utilisé par l'UI mobile.
/// </summary>
public class ParametreMetierParametresParserTests
{
[Theory]
[InlineData("5288012", "#50B04C")] // « Fait » (vert)
[InlineData("15112707", "#E69A03")] // « Confirmé » (orange)
[InlineData("1393623", "#1543D7")] // « Intervention » (bleu)
[InlineData("0", "#000000")]
[InlineData("16777215", "#FFFFFF")]
public void ToHexColor_ConvertitEntierRgbEnHex(string winDevColor, string expected)
{
Assert.Equal(expected, ParametreMetierParametresParser.ToHexColor(winDevColor));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("abc")]
[InlineData("-1")]
[InlineData("-5288012")]
public void ToHexColor_ValeurInvalideOuSysteme_RetourneNull(string? winDevColor)
{
Assert.Null(ParametreMetierParametresParser.ToHexColor(winDevColor));
}
[Fact]
public void ToHexColor_AccepteUnNombreJsonSansDecimale()
{
// Le JSON WinDev peut sérialiser l'entier en nombre (sans guillemets).
Assert.Equal("#50B04C", ParametreMetierParametresParser.ToHexColor("5288012"));
}
[Fact]
public void ParseStatutRdv_ExtraitLaCouleur()
{
StatutRdvParametresDto? statut =
ParametreMetierParametresParser.ParseStatutRdv("{\"sCouleur\":\"15112707\"}");
Assert.NotNull(statut);
Assert.Equal("15112707", statut.Couleur);
}
[Fact]
public void ParseStatutRdv_JsonVide_RetourneNull()
{
Assert.Null(ParametreMetierParametresParser.ParseStatutRdv(""));
Assert.Null(ParametreMetierParametresParser.ParseStatutRdv(null));
Assert.Null(ParametreMetierParametresParser.ParseStatutRdv("not-json"));
}
[Fact]
public void ParseTypeRdv_ExtraitLaCouleur()
{
TypeRdvParametresDto? type =
ParametreMetierParametresParser.ParseTypeRdv("{\"sCouleur\":\"1393623\",\"Envoi_mobile\":true}");
Assert.NotNull(type);
Assert.Equal("1393623", type.Couleur);
Assert.True(type.EnvoiMobile);
}
}
@@ -0,0 +1,87 @@
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Planning;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests des contrats Planning (rendez-vous, participants, annulation) et de
/// leur neutralité totale vis-à-vis des tables physiques HFSQL.
/// </summary>
public class RendezVousContractTests
{
[Fact]
public void RendezVousDetailsDto_IsFreeOfPhysicalNames()
{
var dto = new RendezVousDetailsDto
{
Id = "RDV-001",
Titre = "Visite technique",
Debut = new DateTime(2026, 2, 1, 9, 0, 0),
Fin = new DateTime(2026, 2, 1, 11, 0, 0)
};
string[] forbidden =
[
"Planning_RDV", "RDV_Planning", "PL_PKID", "RDP_GUID", "PKID"
];
foreach (string name in forbidden)
{
Assert.DoesNotContain(name, dto.GetType().Name);
Assert.DoesNotContain(name, dto.Titre);
}
}
[Fact]
public void RendezVousDetailsDto_CanCarryCollaborateursAndContacts()
{
var dto = new RendezVousDetailsDto
{
Id = "RDV-001",
Collaborateurs =
[
new AffectationCollaborateurDto { CollaborateurId = "COL-1", Responsable = true },
new AffectationCollaborateurDto { CollaborateurId = "COL-2", TempsValide = true }
],
Contacts = [new ParticipantRendezVousDto { ContactId = "CT-9", PresenceConfirmee = true }]
};
Assert.Equal(2, dto.Collaborateurs.Count);
Assert.True(dto.Collaborateurs[0].Responsable);
Assert.Single(dto.Contacts);
Assert.True(dto.Contacts[0].PresenceConfirmee);
}
[Fact]
public void RendezVousDetailsDto_CanRepresentAnnulation()
{
var dto = new RendezVousDetailsDto
{
Id = "RDV-002",
Annulation = new AnnulationRendezVousDto
{
DateAnnulation = new DateTime(2026, 2, 1, 8, 0, 0),
Motif = new ReferenceValueDto { Label = "Client indisponible" },
Commentaire = "Reporter la visite"
}
};
Assert.NotNull(dto.Annulation);
Assert.Equal("Client indisponible", dto.Annulation.Motif.Label);
}
[Fact]
public void CreateRendezVousRequest_IsIdenticalForBothSchemas()
{
// Le même contrat sert pour l'ancien et le nouveau schéma.
var request = new CreateRendezVousRequest
{
Titre = "RDV",
Debut = new DateTime(2026, 3, 1, 9, 0, 0),
Fin = new DateTime(2026, 3, 1, 10, 0, 0)
};
Assert.Equal("RDV", request.Titre);
Assert.Equal(new DateTime(2026, 3, 1, 10, 0, 0), request.Fin);
}
}
@@ -0,0 +1,40 @@
using Omika.Core.Contracts.Security;
namespace Omika.Tests.Contracts.Security;
/// <summary>
/// Tests de sécurité du contrat <see cref="LoginRequest"/> : le mot de passe
/// ne doit jamais être exposé par <see cref="LoginRequest.ToString"/>.
/// </summary>
public class LoginRequestTests
{
[Fact]
public void ToString_NeverExposesPassword()
{
const string password = "MotDePasseUltraSecret123!";
var request = new LoginRequest { Login = "jean.dupont", Password = password };
string text = request.ToString();
Assert.DoesNotContain(password, text, StringComparison.Ordinal);
Assert.Contains(request.Login, text, StringComparison.Ordinal);
}
[Fact]
public void ToString_IndicatesWhetherPasswordIsPresent()
{
var withPassword = new LoginRequest { Login = "jean.dupont", Password = "secret" };
var withoutPassword = new LoginRequest { Login = "jean.dupont", Password = null };
Assert.Contains("HasPassword = True", withPassword.ToString(), StringComparison.Ordinal);
Assert.Contains("HasPassword = False", withoutPassword.ToString(), StringComparison.Ordinal);
}
[Fact]
public void ToString_EmptyPasswordIsTreatedAsAbsent()
{
var request = new LoginRequest { Login = "jean.dupont", Password = string.Empty };
Assert.Contains("HasPassword = False", request.ToString(), StringComparison.Ordinal);
}
}
@@ -0,0 +1,328 @@
using Omika.Core.Contracts.Configuration;
namespace Omika.Tests.Contracts;
/// <summary>
/// Tests de la règle centrale d'obligation de saisie d'un type de temps sur un
/// rendez-vous (<see cref="TempsObligatoireRules"/>).
///
/// <para>
/// Les cas de référence sont ceux du métier : pause déjeuner autorisée dans les
/// rendez-vous de type intervention, plage horaire 12:00 → 15:00, saisie
/// obligatoire dès que le rendez-vous dépasse 30 minutes.
/// </para>
/// </summary>
public class TempsObligatoireRulesTests
{
private static readonly DateTime Jour = new(2026, 3, 10);
private static DateTime Heure(int heures, int minutes = 0) => Jour.AddHours(heures).AddMinutes(minutes);
/// <summary>Type de temps « pause déjeuner » : 12:00 → 15:00, minimum 10 minutes.</summary>
private static TypeTempsParametresDto PauseDejeuner(
string? tempsMin = "10",
string? obligatoire = "0030",
string? typeRdv = null,
string? heureDebut = "120000",
string? heureFin = "150000")
=> new()
{
Obligatoire = obligatoire,
HeureDebut = heureDebut,
HeureFin = heureFin,
TempsMin = tempsMin,
TypeRdv = typeRdv
};
/// <summary>Type de rendez-vous « intervention » exigeant une saisie de temps.</summary>
private static TypeRdvParametresDto TypeRdvIntervention(bool saisirTempsObligatoire = true)
=> new() { SaisirTempsObligatoire = saisirTempsObligatoire };
// ===== Déclenchement par le seuil et la plage horaire =====
[Fact]
public void EstObligatoire_RendezVous8h17h_QuiEnglobeLaPlage_EstObligatoire()
{
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(8), Heure(17));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous11h15h_QuiEnglobeLaPlage_EstObligatoire()
{
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(11), Heure(15));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous13h15h_CreneauLibreAvant_NAPasEteObligatoire()
{
// 12:00 → 13:00 reste libre : la pause a pu être prise avant le rendez-vous.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(13), Heure(15));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous11h14h_CreneauLibreApres_NAPasEteObligatoire()
{
// 14:00 → 15:00 reste libre : la pause peut venir après le rendez-vous.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(11), Heure(14));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous12h30_15h_CreneauLibre30Min_AvecMinimum10Min_NAPasEteObligatoire()
{
// 12:00 → 12:30 reste libre, soit 30 minutes : de quoi prendre une pause
// dont la durée minimale est de 10 minutes.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(tempsMin: "10"), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(12, 30), Heure(15));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous12h30_15h_CreneauLibre30Min_AvecMinimum1Heure_EstObligatoire()
{
// Mêmes horaires, mais une pause d'au moins 1 heure ne tient plus dans les
// 30 minutes libres avant le rendez-vous : le collaborateur doit la déclarer.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(tempsMin: "0100"), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(12, 30), Heure(15));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_RendezVous13h30_15h_CreneauLibre1h30_AvecMinimum1Heure_NAPasEteObligatoire()
{
// 12:00 → 13:30 reste libre (1 h 30) : la pause d'une heure peut y être prise.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(tempsMin: "0100"), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(13, 30), Heure(15));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_CreneauLibreInsuffisantDesDeuxCotes_EstObligatoire()
{
// 30 minutes avant et 30 minutes après, mais une pause d'une heure : aucun
// créneau libre n'est assez long, la saisie est obligatoire.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(tempsMin: "0100"), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(12, 30), Heure(14, 30));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_SansTempsMin_NAExigeQueSiAucunCreneauLibre()
{
TypeTempsParametresDto sansMinimum = PauseDejeuner(tempsMin: null);
// 5 minutes libres suffisent à pouvoir prendre le temps ailleurs.
Assert.False(TempsObligatoireRules.EstObligatoire(
sansMinimum, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(12, 5), Heure(15)));
// Aucun créneau libre : la saisie est obligatoire.
Assert.True(TempsObligatoireRules.EstObligatoire(
sansMinimum, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(11), Heure(16)));
}
// ===== Paramètres absents ou nuls : aucune restriction =====
[Fact]
public void EstObligatoire_SeuilVideOuZero_NAPasEteObligatoire()
{
Assert.False(TempsObligatoireRules.EstObligatoire(
PauseDejeuner(obligatoire: null), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(8), Heure(17)));
Assert.False(TempsObligatoireRules.EstObligatoire(
PauseDejeuner(obligatoire: "0"), TypeRdvIntervention(), "T.RDV.INTER", "Intervention",
Heure(8), Heure(17)));
}
[Fact]
public void EstObligatoire_RendezVousPlusCourtQueLeSeuil_NAPasEteObligatoire()
{
// 20 minutes de rendez-vous pour un seuil de 30 minutes.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(12), Heure(12, 20));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_TypeRdvSansSaisieObligatoire_NAPasEteObligatoire()
{
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(saisirTempsObligatoire: false), "T.RDV.INTER", "Intervention",
Heure(8), Heure(17));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_ParametresDuTypeDeTempsAbsents_NAPasEteObligatoire()
{
bool obligatoire = TempsObligatoireRules.EstObligatoire(
null, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(8), Heure(17));
Assert.False(obligatoire);
}
[Fact]
public void EstObligatoire_SansPlageHoraire_LeSeuilSeulDeclenche()
{
// Aucune borne d'horaire : aucune restriction, le seuil décide.
Assert.True(TempsObligatoireRules.EstObligatoire(
PauseDejeuner(heureDebut: null, heureFin: null), TypeRdvIntervention(),
"T.RDV.INTER", "Intervention", Heure(8), Heure(17)));
Assert.True(TempsObligatoireRules.EstObligatoire(
PauseDejeuner(heureDebut: "000000", heureFin: "000000"), TypeRdvIntervention(),
"T.RDV.INTER", "Intervention", Heure(8), Heure(17)));
}
// ===== Premier paramètre : type de rendez-vous autorisé =====
[Fact]
public void EstObligatoire_TypeDeTempsNonAutorisePourCeTypeDeRdv_NAPasEteObligatoire()
{
TypeTempsParametresDto pause = PauseDejeuner(typeRdv: "T.RDV.INTER");
Assert.True(TempsObligatoireRules.EstObligatoire(
pause, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(8), Heure(17)));
Assert.False(TempsObligatoireRules.EstObligatoire(
pause, TypeRdvIntervention(), "T.RDV.SAV", "SAV", Heure(8), Heure(17)));
}
[Fact]
public void EstObligatoire_TypeDeRdvAutoriseParSonLibelle_EstObligatoire()
{
// Génération Batik : le type de temps liste le libellé, le rendez-vous non
// plus (aucune clé métier disponible).
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(typeRdv: "Intervention"), TypeRdvIntervention(), null, "Intervention",
Heure(8), Heure(17));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_AncienCodeTypeAct_EstPrisEnCompte()
{
TypeTempsParametresDto pause = PauseDejeuner(typeRdv: null);
pause = pause with { TypeAct = "T.RDV.INTER" };
Assert.True(TempsObligatoireRules.EstObligatoire(
pause, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(8), Heure(17)));
}
[Fact]
public void EstAutorise_ListeVide_AutoriseTousLesTypesDeRdv()
{
Assert.True(TempsObligatoireRules.EstAutorise(PauseDejeuner(), "T.RDV.SAV", "SAV"));
Assert.True(TempsObligatoireRules.EstAutorise(null, "T.RDV.SAV", "SAV"));
Assert.True(TempsObligatoireRules.EstAutorise(PauseDejeuner(), null, null));
}
[Fact]
public void EstAutorise_TypeDeRdvIndetermine_NAInterditPasLeTypeDeTemps()
{
// Sans type de rendez-vous connu, aucune restriction ne peut être appliquée :
// le type de temps reste saisissable.
Assert.True(TempsObligatoireRules.EstAutorise(PauseDejeuner(typeRdv: "T.RDV.INTER"), null, null));
}
// ===== Plage horaire : bornes partielles, créneau libre =====
[Fact]
public void EstObligatoire_PlageSansHeureDeFin_SEnTendJusquEnFinDeJournee()
{
// Plage 12:00 → fin de journée : un rendez-vous 12:00 → 24:00 la couvre
// entièrement, la pause ne peut plus être prise ailleurs.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(heureDebut: "120000", heureFin: null), TypeRdvIntervention(),
"T.RDV.INTER", "Intervention", Heure(12), Heure(24));
Assert.True(obligatoire);
}
[Fact]
public void EstObligatoire_PlageQuiChevaucheMinuit_EstLueSurLeLendemain()
{
// Plage 22:00 → 02:00 (nuit) et rendez-vous 22:30 → 02:00 : il reste
// 22:00 → 22:30 (30 minutes) pour prendre le temps ailleurs.
TypeTempsParametresDto tempsNuit = PauseDejeuner(
heureDebut: "220000", heureFin: "020000", tempsMin: "10");
Assert.False(TempsObligatoireRules.EstObligatoire(
tempsNuit, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(22, 30), Heure(26)));
// Même plage, rendez-vous 22:00 → 02:00 : plus aucune place libre.
Assert.True(TempsObligatoireRules.EstObligatoire(
tempsNuit, TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(22), Heure(26)));
}
[Fact]
public void PlusGrandCreneauLibreMinutes_RetourneLePlusGrandCote()
{
Assert.Equal(60, TempsObligatoireRules.PlusGrandCreneauLibreMinutes(
PauseDejeuner(), Heure(13), Heure(15)));
Assert.Equal(120, TempsObligatoireRules.PlusGrandCreneauLibreMinutes(
PauseDejeuner(), Heure(14), Heure(16)));
Assert.Equal(0, TempsObligatoireRules.PlusGrandCreneauLibreMinutes(
PauseDejeuner(), Heure(11), Heure(16)));
}
[Fact]
public void PlusGrandCreneauLibreMinutes_SansPlageHoraire_EstIndetermine()
{
Assert.Null(TempsObligatoireRules.PlusGrandCreneauLibreMinutes(
PauseDejeuner(heureDebut: null, heureFin: null), Heure(8), Heure(17)));
Assert.Null(TempsObligatoireRules.PlusGrandCreneauLibreMinutes(null, Heure(8), Heure(17)));
}
[Fact]
public void PlageTexte_FormateLaPlageAutorisee()
{
Assert.Equal("12:00–15:00", TempsObligatoireRules.PlageTexte(PauseDejeuner()));
Assert.Null(TempsObligatoireRules.PlageTexte(PauseDejeuner(heureDebut: null, heureFin: null)));
Assert.Null(TempsObligatoireRules.PlageTexte(null));
}
[Fact]
public void PlusGrandCreneauLibreMinutes_RendezVousHorsPlage_LaisseTouteLaPlage()
{
// Rendez-vous du matin : toute la plage 12:00 → 15:00 reste libre.
Assert.Equal(180, TempsObligatoireRules.PlusGrandCreneauLibreMinutes(
PauseDejeuner(), Heure(8), Heure(11)));
}
[Fact]
public void EstObligatoire_RendezVousTermineAvantLaPlage_NAPasEteObligatoire()
{
// Matinée 8:00 → 9:00 (plus d'une heure de rendez-vous) : la pause reste
// possible dans la plage horaire, elle n'est donc pas obligatoire.
bool obligatoire = TempsObligatoireRules.EstObligatoire(
PauseDejeuner(), TypeRdvIntervention(), "T.RDV.INTER", "Intervention", Heure(8), Heure(9));
Assert.False(obligatoire);
}
}