Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
using Omika.Api.Services;
|
||||
using Omika.Core.Compatibility;
|
||||
using Omika.Core.Constants;
|
||||
using Omika.Core.Contracts.Common;
|
||||
using Omika.Core.Contracts.Mobile.Sync;
|
||||
using Omika.Core.Contracts.Organisation;
|
||||
using Omika.Core.Contracts.Planning;
|
||||
using Omika.Core.Interfaces;
|
||||
using Omika.Core.Interfaces.Mobile;
|
||||
|
||||
using Omika.Core.Domain;
|
||||
|
||||
namespace Omika.Tests.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Tests du filtre de périmètre <see cref="RdvScopeService"/> : compatibilité
|
||||
/// Omika (GUID + PK numérique résolu) sans régression Batik (comparaison par
|
||||
/// PK numérique). Le contrôle des droits n'est jamais contourné : un RDV hors
|
||||
/// périmètre reste filtré.
|
||||
/// </summary>
|
||||
public class RdvScopeServiceTests
|
||||
{
|
||||
// Le token porte le PK numérique du collaborateur (CO_PKID Omika), comme
|
||||
// produit par LicenseService.BuildSuccessAsync.
|
||||
private static readonly MobileTokenIdentity IdentityPk42 = new()
|
||||
{
|
||||
CodeClient = "CLI",
|
||||
DeviceId = "dev",
|
||||
UserId = "1",
|
||||
CollaborateurId = "42"
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Cas1_UtilisateurAvecAcces_RendezVousVisible()
|
||||
{
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 42)],
|
||||
IdentityPk42,
|
||||
Droits(),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas2_UtilisateurSansAcces_RendezVousFiltre()
|
||||
{
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 99)],
|
||||
IdentityPk42,
|
||||
Droits(),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas3_DroitSocietePrincipale_ComportementPreserve()
|
||||
{
|
||||
var service = new FakeCollaborateurService(allowed: [42, 100]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 100)],
|
||||
IdentityPk42,
|
||||
Droits(societePrincipale: true),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal((42L, true, false, false), service.LastCall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas4_DroitSociete_ComportementPreserve()
|
||||
{
|
||||
var service = new FakeCollaborateurService(allowed: [42, 200]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 200)],
|
||||
IdentityPk42,
|
||||
Droits(societes: true),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal((42L, false, true, false), service.LastCall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas5_DroitEquipe_ComportementPreserve()
|
||||
{
|
||||
var service = new FakeCollaborateurService(allowed: [42, 300]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 300)],
|
||||
IdentityPk42,
|
||||
Droits(equipe: true),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal((42L, false, false, true), service.LastCall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas6_CollaborateurOmikaIdentifieParGuid_RendezVousAutoriseVisible()
|
||||
{
|
||||
// Modèle Omika : l'affectation porte le GUID ET le PK numérique résolu.
|
||||
// Le RDV autorisé ne doit plus disparaître.
|
||||
Guid guid = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e");
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 42, guid: guid)],
|
||||
IdentityPk42,
|
||||
Droits(),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas7_Batik_DansPerimetre_Visible()
|
||||
{
|
||||
// Affectation Batik : référence numérique uniquement (FromLegacyId).
|
||||
// Le collaborateur courant (PK 42) est toujours inclus → visible.
|
||||
var rdv = new RendezVousDetailsDto
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("D"),
|
||||
Debut = new DateTime(2026, 8, 19, 9, 0, 0),
|
||||
Collaborateurs =
|
||||
[
|
||||
new AffectationCollaborateurDto
|
||||
{
|
||||
CollaborateurId = "42",
|
||||
SourceReference = LegacyEntityReference.FromLegacyId(42)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[rdv],
|
||||
IdentityPk42,
|
||||
Droits(),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cas7b_Batik_HorsPerimetre_Filtre()
|
||||
{
|
||||
// Comportement attendu Batik (aligné sur MobileBootstrapService) : sans les
|
||||
// 3 droits A.C.PL.*, un RDV affecté à un collaborateur hors périmètre est
|
||||
// filtré (le filtre par PK numérique s'applique). Avant le correctif, ce
|
||||
// cas était silencieusement fail-open (RDV visible à tort).
|
||||
var rdv = new RendezVousDetailsDto
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("D"),
|
||||
Debut = new DateTime(2026, 8, 19, 9, 0, 0),
|
||||
Collaborateurs =
|
||||
[
|
||||
new AffectationCollaborateurDto
|
||||
{
|
||||
CollaborateurId = "99",
|
||||
SourceReference = LegacyEntityReference.FromLegacyId(99)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[rdv],
|
||||
IdentityPk42,
|
||||
Droits(),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TroisDroits_AucunFiltre()
|
||||
{
|
||||
// Les 3 droits présents → aucune restriction (comportement historique).
|
||||
var service = new FakeCollaborateurService(allowed: [42]);
|
||||
var result = await RdvScopeService.ApplyScopeAsync(
|
||||
[RdvOmika(numericId: 99)],
|
||||
IdentityPk42,
|
||||
Droits(societePrincipale: true, societes: true, equipe: true),
|
||||
service,
|
||||
new FakeIdentityResolver(),
|
||||
default);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Null(service.LastCall);
|
||||
}
|
||||
|
||||
private static RendezVousDetailsDto RdvOmika(long numericId, Guid? guid = null)
|
||||
=> new()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("D"),
|
||||
Debut = new DateTime(2026, 8, 19, 9, 0, 0),
|
||||
Collaborateurs =
|
||||
[
|
||||
new AffectationCollaborateurDto
|
||||
{
|
||||
CollaborateurId = guid?.ToString("D") ?? numericId.ToString(),
|
||||
SourceReference = new LegacyEntityReference(
|
||||
DatabaseSchemaVersion.Omika,
|
||||
NumericId: numericId,
|
||||
EntityGuid: guid)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
private static IReadOnlyDictionary<string, bool> Droits(
|
||||
bool societePrincipale = false,
|
||||
bool societes = false,
|
||||
bool equipe = false)
|
||||
=> new Dictionary<string, bool>
|
||||
{
|
||||
[MobilePermissionKeys.PlanningSocietePrincipale] = societePrincipale,
|
||||
[MobilePermissionKeys.PlanningSocietes] = societes,
|
||||
[MobilePermissionKeys.PlanningEquipe] = equipe
|
||||
};
|
||||
|
||||
private sealed class FakeIdentityResolver : IMobileEntityIdentityResolver
|
||||
{
|
||||
public Guid CreateCanonicalIdentity(string entityType, PhysicalEntityIdentity physical)
|
||||
=> physical.Guid ?? Guid.NewGuid();
|
||||
|
||||
public PhysicalEntityIdentity? ResolvePhysicalIdentity(string entityType, Guid publicGuid)
|
||||
=> new PhysicalEntityIdentity { Guid = publicGuid };
|
||||
|
||||
public bool IsCanonical(string entityType, Guid publicGuid)
|
||||
=> publicGuid != Guid.Empty;
|
||||
}
|
||||
|
||||
private sealed class FakeCollaborateurService : ICollaborateurService
|
||||
{
|
||||
private readonly IReadOnlyList<long> _allowed;
|
||||
|
||||
public FakeCollaborateurService(IReadOnlyList<long> allowed)
|
||||
=> _allowed = allowed;
|
||||
|
||||
public (long CollaborateurId, bool SocietePrincipale, bool Societes, bool Equipe)? LastCall { get; private set; }
|
||||
|
||||
public Task<IReadOnlyList<long>> GetAllowedCollaborateurIdsAsync(
|
||||
long collaborateurId,
|
||||
bool includeSocietePrincipale,
|
||||
bool includeSocietes,
|
||||
bool includeEquipe,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
LastCall = (collaborateurId, includeSocietePrincipale, includeSocietes, includeEquipe);
|
||||
return Task.FromResult(_allowed);
|
||||
}
|
||||
|
||||
public Task<PagedResult<CollaborateurSummaryDto>> SearchAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<PagedResult<CollaborateurDetailsDto>> SearchDetailsAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<CollaborateurDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<CollaborateurDetailsDto> CreateAsync(CreateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<CollaborateurDetailsDto> UpdateAsync(string id, UpdateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<IReadOnlyList<long>> GetSocieteIdsAsync(long collaborateurId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<IReadOnlyList<RendezVousDetailsDto>> GetRendezVousAsync(string collaborateurId, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<IReadOnlyList<RendezVousDetailsDto>> GetRendezVousForCollaborateursAsync(IReadOnlyList<string> collaborateurIds, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user