299 lines
11 KiB
C#
299 lines
11 KiB
C#
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();
|
|
}
|
|
}
|