using System.Diagnostics; using Omika.App.Local.Abstractions; using Omika.App.Local.Services; namespace Omika.Tests.Mobile.Local; /// /// Tests de la surveillance de fond de la file Outbox : les données saisies /// localement doivent partir automatiquement dès que le réseau est disponible, /// sans action de l'utilisateur (la synchronisation manuelle reste disponible). /// public class BackgroundSyncServiceTests { /// Connectivité simulée, avec comptage des interrogations. private sealed class CountingNetwork : INetworkConnectivity { private int _probes; public bool Online { get; set; } = true; public int Probes => Volatile.Read(ref _probes); public Task IsOnlineAsync(CancellationToken cancellationToken = default) { Interlocked.Increment(ref _probes); return Task.FromResult(Online); } } private static readonly TimeSpan Court = TimeSpan.FromMilliseconds(20); [Fact] public async Task CycleAutomatique_QuandDonneesEnAttenteEtReseauDisponible() { var network = new CountingNetwork(); var service = new BackgroundSyncService(network, interval: Court); int pending = 1; int cycles = 0; var premierCycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); service.Start( cancellationToken => Task.FromResult(Volatile.Read(ref pending)), _ => { Interlocked.Increment(ref cycles); // La file est vidée par le cycle (comportement réel : les // opérations envoyées quittent l'état « en attente »). Volatile.Write(ref pending, 0); premierCycle.TrySetResult(); return Task.CompletedTask; }); await premierCycle.Task.WaitAsync(TimeSpan.FromSeconds(5)); // Plus rien en attente : la surveillance ne relance aucun cycle. await Task.Delay(200); Assert.Equal(1, Volatile.Read(ref cycles)); await service.StopAsync(); } [Fact] public async Task AucunCycle_HorsLigne() { var network = new CountingNetwork { Online = false }; var service = new BackgroundSyncService(network, interval: Court); int cycles = 0; service.Start( _ => Task.FromResult(1), _ => { Interlocked.Increment(ref cycles); return Task.CompletedTask; }); // Plusieurs tours ont lieu (données en attente) : aucun cycle réseau. await Task.Delay(250); await service.StopAsync(); Assert.Equal(0, Volatile.Read(ref cycles)); Assert.True(network.Probes > 0, "la connectivité doit être vérifiée à chaque tour"); } [Fact] public async Task AucunCycle_SansDonneesEnAttente_EtAucuneInterrogationReseau() { var network = new CountingNetwork(); var service = new BackgroundSyncService(network, interval: Court); int cycles = 0; service.Start( _ => Task.FromResult(0), _ => { Interlocked.Increment(ref cycles); return Task.CompletedTask; }); await Task.Delay(250); await service.StopAsync(); Assert.Equal(0, Volatile.Read(ref cycles)); // Rien à envoyer : inutile d'interroger le réseau (économie batterie). Assert.Equal(0, network.Probes); } [Fact] public async Task RequestSync_DeclencheLeCycleSansAttendreLeDelaiPeriodique() { var network = new CountingNetwork(); // Délai périodique très long : seul le réveil explicite peut déclencher. var service = new BackgroundSyncService(network, interval: TimeSpan.FromMinutes(10)); int cycles = 0; var cycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); service.Start( _ => Task.FromResult(1), _ => { Interlocked.Increment(ref cycles); cycle.TrySetResult(); return Task.CompletedTask; }); // Écriture locale (ex. création d'un compte rendu) : réveil immédiat. service.RequestSync(); var sw = Stopwatch.StartNew(); await cycle.Task.WaitAsync(TimeSpan.FromSeconds(5)); sw.Stop(); await service.StopAsync(); Assert.Equal(1, Volatile.Read(ref cycles)); Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5), $"cycle déclenché en {sw.Elapsed}"); } [Fact] public async Task UnCycleEnErreurNArretePasLaSurveillance() { var network = new CountingNetwork(); var service = new BackgroundSyncService(network, interval: Court); int cycles = 0; var deuxiemeCycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); service.Start( _ => Task.FromResult(1), _ => { if (Interlocked.Increment(ref cycles) == 1) { throw new InvalidOperationException("échec réseau simulé"); } deuxiemeCycle.TrySetResult(); return Task.CompletedTask; }); // La boucle survit à l'échec et réessaie d'elle-même. await deuxiemeCycle.Task.WaitAsync(TimeSpan.FromSeconds(5)); await service.StopAsync(); Assert.True(Volatile.Read(ref cycles) >= 2, $"cycles observés : {Volatile.Read(ref cycles)}"); } [Fact] public async Task RepliExponentiel_QuandLaFileNeProgressePas() { var network = new CountingNetwork(); var service = new BackgroundSyncService( network, interval: TimeSpan.FromMilliseconds(40), maxBackoff: TimeSpan.FromMilliseconds(320)); int cycles = 0; // La file ne diminue jamais (opération bloquée) : la surveillance doit // espacer ses tentatives au lieu de marteler le serveur. service.Start( _ => Task.FromResult(5), _ => { Interlocked.Increment(ref cycles); return Task.CompletedTask; }); await Task.Delay(1000); await service.StopAsync(); int observed = Volatile.Read(ref cycles); Assert.True(observed >= 2, $"au moins deux tentatives attendues, observées : {observed}"); // Sans repli, ~25 cycles seraient exécutés sur la même fenêtre. Assert.True(observed <= 8, $"repli exponentiel attendu, cycles observés : {observed}"); } [Fact] public async Task Arret_StoppeLaSurveillance() { var network = new CountingNetwork(); var service = new BackgroundSyncService(network, interval: Court); int cycles = 0; service.Start( _ => Task.FromResult(1), _ => { Interlocked.Increment(ref cycles); return Task.CompletedTask; }); await Task.Delay(150); await service.StopAsync(); int apresArret = Volatile.Read(ref cycles); await Task.Delay(200); Assert.Equal(apresArret, Volatile.Read(ref cycles)); } [Fact] public async Task DemarrageIdempotent_UneSeuleBoucleDeSurveillance() { var network = new CountingNetwork(); var service = new BackgroundSyncService(network, interval: Court); int concurrent = 0; int maximum = 0; Func> pending = _ => Task.FromResult(1); Func action = async _ => { int enCours = Interlocked.Increment(ref concurrent); if (enCours > Volatile.Read(ref maximum)) { Volatile.Write(ref maximum, enCours); } await Task.Delay(60); Interlocked.Decrement(ref concurrent); }; // Deux démarrages simultanés ne doivent pas ouvrir deux boucles qui // exécuteraient deux cycles en parallèle. service.Start(pending, action); service.Start(pending, action); await Task.Delay(400); await service.StopAsync(); Assert.Equal(1, Volatile.Read(ref maximum)); } }