299 lines
11 KiB
C#
299 lines
11 KiB
C#
using System.Text.Json;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Local.Repositories;
|
|
using Omika.App.Local.Services;
|
|
using Omika.Core.Contracts.Common;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
using Omika.Core.Contracts.Planning;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du service local SuiviTemps (lecture snapshot SQLite + création
|
|
/// offline-first). Vérifie la lecture, le tri, l'ignorance des payloads
|
|
/// invalides, la conservation du GUID public, le filtre par intervention et la
|
|
/// création offline-first (écriture locale + Outbox CREATE, Valide toujours
|
|
/// false).
|
|
/// </summary>
|
|
public class SuiviTempsLocalServiceTests : LocalTestBase
|
|
{
|
|
private readonly SuiviTempsLocalService _service;
|
|
|
|
public SuiviTempsLocalServiceTests()
|
|
{
|
|
_service = new SuiviTempsLocalService(
|
|
SuiviTempsRepository, OutboxService, ApiClient,
|
|
SuiviTempsDraftRepository, RendezVousTempsValidationRepository,
|
|
new RendezVousDraftReferenceRepository(Database), SessionService);
|
|
}
|
|
|
|
private static string Serialize<T>(T value) => JsonSerializer.Serialize(value);
|
|
|
|
private static LocalSuiviTemps CreateSuiviTemps(
|
|
Guid id,
|
|
DateTime date,
|
|
int dureeMinutes,
|
|
Guid? interventionId = null,
|
|
Guid? collaborateurId = null,
|
|
Guid? rendezVousId = null,
|
|
string? description = null)
|
|
{
|
|
var dto = new SuiviTempsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Date = date,
|
|
DureeMinutes = dureeMinutes,
|
|
InterventionId = interventionId?.ToString(),
|
|
CollaborateurId = collaborateurId?.ToString(),
|
|
RendezVousId = rendezVousId?.ToString(),
|
|
Description = description,
|
|
TypeTemps = new ReferenceValueDto { Label = "Déplacement" },
|
|
Valide = true
|
|
};
|
|
return new LocalSuiviTemps
|
|
{
|
|
Id = id,
|
|
InterventionId = interventionId,
|
|
CollaborateurId = collaborateurId,
|
|
RendezVousId = rendezVousId,
|
|
PayloadJson = Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAll_Aucun_ListeVide()
|
|
{
|
|
// Act
|
|
IReadOnlyList<SuiviTempsDto> result = await _service.GetAllAsync();
|
|
|
|
// Assert
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAll_Un_RetourneLaListe()
|
|
{
|
|
// Arrange
|
|
Guid id = Guid.NewGuid();
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(id, new DateTime(2026, 8, 10), 60));
|
|
|
|
// Act
|
|
IReadOnlyList<SuiviTempsDto> result = await _service.GetAllAsync();
|
|
|
|
// Assert
|
|
var item = Assert.Single(result);
|
|
Assert.Equal(id.ToString(), item.Id);
|
|
Assert.Equal(60, item.DureeMinutes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAll_Plusieurs_TrieParDatePuisDuree()
|
|
{
|
|
// Arrange
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 15), 30));
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 10), 90));
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 10), 45));
|
|
|
|
// Act
|
|
IReadOnlyList<SuiviTempsDto> result = await _service.GetAllAsync();
|
|
|
|
// Assert : tri déterministe par date puis durée.
|
|
Assert.Equal(3, result.Count);
|
|
Assert.Equal(45, result[0].DureeMinutes);
|
|
Assert.Equal(90, result[1].DureeMinutes);
|
|
Assert.Equal(30, result[2].DureeMinutes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAll_PayloadInvalide_Ignore()
|
|
{
|
|
// Arrange
|
|
await SuiviTempsRepository.UpsertAsync(new LocalSuiviTemps
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PayloadJson = "not-json",
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
// Act
|
|
IReadOnlyList<SuiviTempsDto> result = await _service.GetAllAsync();
|
|
|
|
// Assert : l'entrée invalide est ignorée sans erreur.
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByGuid_Trouve_RetourneLeDto()
|
|
{
|
|
// Arrange
|
|
Guid id = Guid.NewGuid();
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(id, new DateTime(2026, 8, 10), 60));
|
|
|
|
// Act
|
|
SuiviTempsDto? result = await _service.GetByGuidAsync(id);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(id.ToString(), result!.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByGuid_Introuvable_RetourneNull()
|
|
{
|
|
// Act
|
|
SuiviTempsDto? result = await _service.GetByGuidAsync(Guid.NewGuid());
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIntervention_FiltreParIntervention()
|
|
{
|
|
// Arrange
|
|
Guid interventionA = Guid.NewGuid();
|
|
Guid interventionB = Guid.NewGuid();
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 10), 60, interventionId: interventionA));
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 11), 30, interventionId: interventionA));
|
|
await SuiviTempsRepository.UpsertAsync(CreateSuiviTemps(Guid.NewGuid(), new DateTime(2026, 8, 12), 90, interventionId: interventionB));
|
|
|
|
// Act
|
|
IReadOnlyList<SuiviTempsDto> result = await _service.GetByInterventionAsync(interventionA);
|
|
|
|
// Assert : seuls les suivis de l'intervention A sont retournés.
|
|
Assert.Equal(2, result.Count);
|
|
Assert.All(result, s => Assert.Equal(interventionA.ToString(), s.InterventionId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_EcritLocalementEtEnqueueOutboxCreate()
|
|
{
|
|
// Arrange
|
|
var payload = new SuiviTempsSyncPayload
|
|
{
|
|
CollaborateurId = Guid.NewGuid().ToString(),
|
|
InterventionId = Guid.NewGuid().ToString(),
|
|
Date = new DateTime(2026, 9, 1, 9, 0, 0),
|
|
DureeMinutes = 45,
|
|
TypeTempsKey = "DEPLACEMENT",
|
|
Description = "Déplacement client"
|
|
};
|
|
|
|
// Act
|
|
Guid requestedGuid = await _service.CreateAsync(payload);
|
|
|
|
// Assert : l'entité locale est créée avec le GUID public (A).
|
|
LocalSuiviTemps? local = await SuiviTempsRepository.GetByIdAsync(requestedGuid);
|
|
Assert.NotNull(local);
|
|
SuiviTempsDto? dto = JsonSerializer.Deserialize<SuiviTempsDto>(local!.PayloadJson);
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(45, dto!.DureeMinutes);
|
|
Assert.Equal(payload.InterventionId, dto.InterventionId);
|
|
Assert.Equal(payload.CollaborateurId, dto.CollaborateurId);
|
|
|
|
// Assert : Valide est toujours false (le mobile ne peut pas le fixer).
|
|
Assert.False(dto.Valide);
|
|
|
|
// Assert : une opération Outbox CREATE est enqueue (RequestedEntityGuid = A).
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
var op = Assert.Single(ready);
|
|
Assert.Equal(SyncOperationType.Create, op.OperationType);
|
|
Assert.Equal(requestedGuid, op.RequestedEntityGuid);
|
|
Assert.Equal("SuiviTemps", op.EntityType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_DeuxCreations_DeuxOperationIdsDistincts()
|
|
{
|
|
// Arrange
|
|
var payloadA = new SuiviTempsSyncPayload { Date = new DateTime(2026, 9, 1), DureeMinutes = 30 };
|
|
var payloadB = new SuiviTempsSyncPayload { Date = new DateTime(2026, 9, 2), DureeMinutes = 60 };
|
|
|
|
// Act
|
|
Guid guidA = await _service.CreateAsync(payloadA);
|
|
Guid guidB = await _service.CreateAsync(payloadB);
|
|
|
|
// Assert : deux identités mobiles distinctes et deux opérations Outbox.
|
|
Assert.NotEqual(guidA, guidB);
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
Assert.Equal(2, ready.Count);
|
|
Assert.Equal(2, ready.Select(o => o.OperationId).Distinct().Count());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateRendezVous_EnvoieBrouillonsPuisMarqueTempsValide()
|
|
{
|
|
// Arrange : un brouillon en attente rattaché au rendez-vous, et un
|
|
// collaborateur courant dans le bootstrap (nécessaire au TempsValide).
|
|
Guid rendezVousGuid = Guid.NewGuid();
|
|
Guid collaborateurGuid = Guid.NewGuid();
|
|
Guid interventionGuid = Guid.NewGuid();
|
|
|
|
SessionService.Bootstrap = new LocalBootstrapState
|
|
{
|
|
Collaborateur = new Omika.Core.Contracts.Organisation.CollaborateurDetailsDto
|
|
{
|
|
Id = collaborateurGuid.ToString("D")
|
|
}
|
|
};
|
|
|
|
await SuiviTempsDraftRepository.UpsertAsync(new SuiviTempsDraftEntry
|
|
{
|
|
RendezVousId = rendezVousGuid,
|
|
TypeTempsKey = "T.TEMPS.INTER",
|
|
CollaborateurId = collaborateurGuid,
|
|
InterventionId = interventionGuid,
|
|
Date = new DateTime(2026, 9, 1, 9, 0, 0),
|
|
DureeMinutes = 60,
|
|
EstEnvoye = false
|
|
});
|
|
|
|
// Act
|
|
await _service.ValidateRendezVousAsync(rendezVousGuid);
|
|
|
|
// Assert : le brouillon est marqué envoyé.
|
|
IReadOnlyList<SuiviTempsDraftEntry> drafts =
|
|
await SuiviTempsDraftRepository.GetByRendezVousAsync(rendezVousGuid);
|
|
Assert.True(Assert.Single(drafts).EstEnvoye);
|
|
|
|
// Assert : une opération Outbox SuiviTemps CREATE et une RendezVous UPDATE.
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
OutboxOperation create = Assert.Single(
|
|
ready, o => o.EntityType == "SuiviTemps" && o.OperationType == SyncOperationType.Create);
|
|
|
|
// L'identité mobile de la saisie (RequestedEntityGuid) est un GUID neuf,
|
|
// distinct du rendez-vous qui porte la saisie.
|
|
Assert.NotEqual(rendezVousGuid, create.RequestedEntityGuid);
|
|
|
|
Assert.Contains(ready, o => o.EntityType == "RendezVous" && o.OperationType == SyncOperationType.Update);
|
|
|
|
// Assert : le marqueur local de validation est posé.
|
|
Assert.True(await _service.IsValideAsync(rendezVousGuid));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateRendezVous_SansBrouillon_NEnvoiePasDeSuiviTemps()
|
|
{
|
|
// Arrange : aucun brouillon. Seul le marquage TempsValide doit partir.
|
|
Guid rendezVousGuid = Guid.NewGuid();
|
|
Guid collaborateurGuid = Guid.NewGuid();
|
|
|
|
SessionService.Bootstrap = new LocalBootstrapState
|
|
{
|
|
Collaborateur = new Omika.Core.Contracts.Organisation.CollaborateurDetailsDto
|
|
{
|
|
Id = collaborateurGuid.ToString("D")
|
|
}
|
|
};
|
|
|
|
// Act
|
|
await _service.ValidateRendezVousAsync(rendezVousGuid);
|
|
|
|
// Assert : aucune opération SuiviTemps CREATE, mais une RendezVous UPDATE.
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
Assert.DoesNotContain(ready, o => o.EntityType == "SuiviTemps");
|
|
Assert.Contains(ready, o => o.EntityType == "RendezVous" && o.OperationType == SyncOperationType.Update);
|
|
}
|
|
}
|