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