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