Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Omika.Core.Configuration;
|
||||
using Omika.Core.Constants;
|
||||
using Omika.Core.Contracts.Mobile.Sync;
|
||||
using Omika.Core.Interfaces.Mobile;
|
||||
using Omika.Core.Observability.Deployment;
|
||||
using Omika.Core.Observability.Identity;
|
||||
using Omika.Core.Observability.Outbox;
|
||||
using Omika.Data.Connection;
|
||||
using Omika.Worker;
|
||||
|
||||
namespace Omika.Tests.Mobile;
|
||||
|
||||
/// <summary>
|
||||
/// Tâche 13-E — instrumentation du Worker métier (SyncProcessor) dans le pipeline
|
||||
/// Diagnostics. Vérifie l'émission réelle de :
|
||||
/// <list type="bullet">
|
||||
/// <item>OMK-WKR-SYNC-001 (succès, Info/P3) ;</item>
|
||||
/// <item>OMK-WKR-SYNC-002 (échec définitif, Error/P1) ;</item>
|
||||
/// <item>contexte ClientCode/ServerInstance/Environment toujours issu du provider
|
||||
/// (jamais de l'opération/payload) ;</item>
|
||||
/// <item>fail-open absolu (provider sans ClientCode, store en panne) ;</item>
|
||||
/// <item>corrélation par OperationId (aucun faux TraceId inventé).</item>
|
||||
/// </list>
|
||||
/// Aucune base HFSQL/PostgreSQL réelle : fakes en mémoire uniquement.
|
||||
/// </summary>
|
||||
public class WorkerSyncDiagnosticsTests
|
||||
{
|
||||
private static SyncOptions DefaultOptions() => new()
|
||||
{
|
||||
MaxRetryCount = 30,
|
||||
RetryDelaySeconds = 120,
|
||||
LogRetentionDays = 30
|
||||
};
|
||||
|
||||
private static SyncEnvelope CreateEnvelope(
|
||||
SyncOperationType type,
|
||||
Guid? operationId = null,
|
||||
int retryCount = 0,
|
||||
string? payload = null) => new()
|
||||
{
|
||||
OperationId = operationId ?? Guid.NewGuid(),
|
||||
DeviceId = "device-1",
|
||||
ClientId = "FAKE-CLIENT-ID",
|
||||
UserId = "user-1",
|
||||
CollaborateurId = "collab-1",
|
||||
EntityType = "Client",
|
||||
OperationType = type,
|
||||
EntityGuid = Guid.NewGuid(),
|
||||
RetryCount = retryCount,
|
||||
Payload = payload ?? "{}",
|
||||
ReceivedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
private static FakeDeploymentContextProvider Provider(
|
||||
string? clientCode = "CLIENT-A",
|
||||
string serverInstance = "WKR-SRV-01",
|
||||
string environment = "Production") =>
|
||||
new() { Context = new DiagnosticDeploymentContext { ClientCode = clientCode, ServerInstance = serverInstance, Environment = environment } };
|
||||
|
||||
private static SyncProcessor CreateProcessor(
|
||||
FakeSyncRepository repo,
|
||||
ISyncOperationHandler handler,
|
||||
SyncOptions? options = null,
|
||||
IWebServiceTransactionContext? transactionContext = null,
|
||||
IRemoteDiagnosticIngestionStore? store = null,
|
||||
IDiagnosticDeploymentContextProvider? provider = null)
|
||||
{
|
||||
var registry = new SyncOperationHandlerRegistry(new[] { handler });
|
||||
return new SyncProcessor(
|
||||
repo,
|
||||
registry,
|
||||
new DefaultSyncOperationHandler(),
|
||||
options ?? DefaultOptions(),
|
||||
NullLogger<SyncProcessor>.Instance,
|
||||
transactionContext ?? new FakeTransaction(),
|
||||
new WorkerDiagnosticEmitter(
|
||||
store ?? new FakeDiagnosticStore(),
|
||||
provider ?? Provider()));
|
||||
}
|
||||
|
||||
// --------------------------- E1 : succès -----------------------------------
|
||||
[Fact]
|
||||
public async Task E1_Success_EmitsWorkerSyncProcessed_WithDeploymentContext()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var provider = Provider();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: provider);
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
Assert.True(handled);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
|
||||
Assert.Equal("OMK-WKR-SYNC-001", evt.EventCode);
|
||||
Assert.Equal("Info", evt.Level);
|
||||
Assert.Equal("P3", evt.Priority);
|
||||
Assert.Equal(SyncProcessor.DiagnosticComponent, evt.Component);
|
||||
Assert.Equal("CLIENT-A", call.Context!.ClientCode);
|
||||
Assert.Equal("WKR-SRV-01", call.Context.ServerInstance);
|
||||
Assert.Equal("Production", call.Context.Environment);
|
||||
}
|
||||
|
||||
// --------------------------- E2 : échec définitif --------------------------
|
||||
[Fact]
|
||||
public async Task E2_PermanentFailure_EmitsWorkerSyncFailed_ErrorP1()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.PermanentFailure("NO_HANDLER", "Aucun handler"));
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Contains(envelope.OperationId, repo.Rejected); // mécanisme métier intact.
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
|
||||
Assert.Equal("OMK-WKR-SYNC-002", evt.EventCode);
|
||||
Assert.Equal("Error", evt.Level);
|
||||
Assert.Equal("P1", evt.Priority);
|
||||
Assert.Equal("NO_HANDLER", evt.ErrorCode);
|
||||
Assert.False(evt.Success);
|
||||
Assert.Equal("CLIENT-A", call.Context!.ClientCode);
|
||||
Assert.Equal("WKR-SRV-01", call.Context.ServerInstance);
|
||||
Assert.Equal("Production", call.Context.Environment);
|
||||
}
|
||||
|
||||
// --------------------------- E3 : fail-open provider -----------------------
|
||||
[Fact]
|
||||
public async Task E3_ProviderWithoutClientCode_EventStillWrittenWithNull()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider(clientCode: null));
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
Assert.True(handled);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
Assert.Null(call.Context!.ClientCode);
|
||||
Assert.Equal("WKR-SRV-01", call.Context.ServerInstance);
|
||||
Assert.Equal("Production", call.Context.Environment);
|
||||
}
|
||||
|
||||
// --------------------------- E4 : store en panne ---------------------------
|
||||
[Fact]
|
||||
public async Task E4_StoreFailure_DoesNotBreakBusinessProcessing()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore(throwOnStore: true);
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
// Fail-open : la panne du store n'a pas transformé le succès métier en échec.
|
||||
Assert.True(handled);
|
||||
Assert.Contains(envelope.OperationId, repo.Succeeded);
|
||||
}
|
||||
|
||||
// --------------------------- E5 : contexte non falsifiable -----------------
|
||||
[Fact]
|
||||
public async Task E5_OperationIdentities_NeverReplaceProviderClientCode()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider(clientCode: "CLIENT-A"));
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
Assert.Equal("CLIENT-A", call.Context!.ClientCode);
|
||||
|
||||
string json = JsonSerializer.Serialize(call.Batch);
|
||||
Assert.DoesNotContain("FAKE-CLIENT-ID", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("device-1", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("user-1", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("collab-1", json, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// --------------------------- E6 : ServerInstance Worker --------------------
|
||||
[Fact]
|
||||
public async Task E6_ServerInstance_ComesFromProvider_NotOtherComponents()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider(serverInstance: "WKR-SRV-01"));
|
||||
|
||||
await processor.ProcessOperationAsync(CreateEnvelope(SyncOperationType.Create));
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
Assert.Equal("WKR-SRV-01", call.Context!.ServerInstance);
|
||||
Assert.NotEqual("API-SRV-01", call.Context.ServerInstance);
|
||||
}
|
||||
|
||||
// --------------------------- E7 : Environment Worker -----------------------
|
||||
[Fact]
|
||||
public async Task E7_Environment_ComesFromProvider()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider(environment: "Staging"));
|
||||
|
||||
await processor.ProcessOperationAsync(CreateEnvelope(SyncOperationType.Create));
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
Assert.Equal("Staging", call.Context!.Environment);
|
||||
}
|
||||
|
||||
// --------------------------- E8 : corrélation ------------------------------
|
||||
[Fact]
|
||||
public async Task E8_OperationIdPreserved_NoFakeTraceIdInvented()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
|
||||
// Corrélation réelle : OperationId transporté de bout en bout (mobile → API
|
||||
// → TOSRV → Worker). Aucun TraceId n'est transporté (SyncEnvelope n'en porte
|
||||
// pas) : on n'en invente PAS un faux.
|
||||
Assert.Equal(envelope.OperationId, evt.OperationId);
|
||||
Assert.Null(evt.TraceId);
|
||||
Assert.Null(evt.SessionId);
|
||||
}
|
||||
|
||||
// --------------------------- E9 : concurrence ------------------------------
|
||||
[Fact]
|
||||
public async Task E9_ConcurrentOperations_DoNotContaminateCorrelation()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
|
||||
var envelopeA = CreateEnvelope(SyncOperationType.Create);
|
||||
var envelopeB = CreateEnvelope(SyncOperationType.Update);
|
||||
var envelopeC = CreateEnvelope(SyncOperationType.Delete);
|
||||
|
||||
await Task.WhenAll(
|
||||
processor.ProcessOperationAsync(envelopeA),
|
||||
processor.ProcessOperationAsync(envelopeB),
|
||||
processor.ProcessOperationAsync(envelopeC));
|
||||
|
||||
Assert.Equal(3, store.Calls.Count);
|
||||
var operationIds = store.Calls.Select(c => Assert.Single(c.Batch.Events).OperationId).ToArray();
|
||||
Assert.Contains(envelopeA.OperationId, operationIds);
|
||||
Assert.Contains(envelopeB.OperationId, operationIds);
|
||||
Assert.Contains(envelopeC.OperationId, operationIds);
|
||||
Assert.Equal(3, operationIds.Distinct().Count());
|
||||
|
||||
// Le contexte de déploiement est commun (instance = 1 client) : valeurs stables.
|
||||
Assert.All(store.Calls, c => Assert.Equal("WKR-SRV-01", c.Context!.ServerInstance));
|
||||
Assert.All(store.Calls, c => Assert.Equal("Production", c.Context!.Environment));
|
||||
}
|
||||
|
||||
// --------------------------- E10 : absence de secrets ----------------------
|
||||
[Fact]
|
||||
public async Task E10_NoSecrets_LeakIntoProducedEvents()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
|
||||
// Payload métier volontairement truffé de valeurs sensibles : il ne doit
|
||||
// JAMAIS être copié dans l'événement Diagnostics produit.
|
||||
string payload = """
|
||||
{"password":"S3CR3T","token":"Bearer eyJhbGciOiJIUzI1NiJ9","hmacKey":"h0000","connectionString":"Server=x;Password=y","CL_CleAPIInstall":"CLE-SECRET"}
|
||||
""";
|
||||
await processor.ProcessOperationAsync(CreateEnvelope(SyncOperationType.Create, payload: payload));
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
string json = JsonSerializer.Serialize(call.Batch);
|
||||
string contextJson = JsonSerializer.Serialize(call.Context);
|
||||
|
||||
Assert.DoesNotContain("S3CR3T", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Bearer", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("eyJhbGciOi", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("hmacKey", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("connectionString", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("password", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("CL_CleAPIInstall", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("CLE-SECRET", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("CLE-SECRET", contextJson, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// --------------------------- E11 : AppVersion ------------------------------
|
||||
[Fact]
|
||||
public async Task E11_AppVersion_UsesExistingConvention_NotHardcoded()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.Success());
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
|
||||
await processor.ProcessOperationAsync(CreateEnvelope(SyncOperationType.Create));
|
||||
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
|
||||
// Convention Diagnostics existante : ApplicationConstants.Version (source de
|
||||
// vérité unique de la solution), jamais une chaîne "1.0.0.0" en dur ici.
|
||||
Assert.Equal(ApplicationConstants.Version, evt.AppVersion);
|
||||
}
|
||||
|
||||
// --------------------------- Retry : échec intermédiaire → succès final ----
|
||||
[Fact]
|
||||
public async Task Retry_IntermediateFailure_ThenSuccess_EmitsTransientWarningThenSuccess()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.TransientFailure("DB_BUSY"));
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
// Tentative 1 : échec temporaire → retry, UN événement Warning/P2 (retry visible).
|
||||
bool first = await processor.ProcessOperationAsync(envelope);
|
||||
Assert.False(first);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) transientCall = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto transientEvt = Assert.Single(transientCall.Batch.Events);
|
||||
Assert.Equal("OMK-WKR-SYNC-002", transientEvt.EventCode);
|
||||
Assert.Equal("Warning", transientEvt.Level);
|
||||
Assert.Equal("P2", transientEvt.Priority);
|
||||
Assert.False(transientEvt.Success);
|
||||
Assert.Equal(envelope.OperationId, transientEvt.OperationId); // OperationId stable.
|
||||
|
||||
// Tentative 2 : succès → un DEUXIÈME événement 001 (Info/P3).
|
||||
handler.Result = SyncOperationResult.Success();
|
||||
var envelopeRetry = envelope with { RetryCount = 1 };
|
||||
bool second = await processor.ProcessOperationAsync(envelopeRetry);
|
||||
Assert.True(second);
|
||||
|
||||
Assert.Equal(2, store.Calls.Count);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) successCall = store.Calls[1];
|
||||
DiagnosticEventDto successEvt = Assert.Single(successCall.Batch.Events);
|
||||
Assert.Equal("OMK-WKR-SYNC-001", successEvt.EventCode);
|
||||
Assert.Equal("Info", successEvt.Level);
|
||||
Assert.Equal("P3", successEvt.Priority);
|
||||
Assert.Equal(envelope.OperationId, successEvt.OperationId); // OperationId stable.
|
||||
}
|
||||
|
||||
// --------------------------- Retry : échec définitif après max -------------
|
||||
[Fact]
|
||||
public async Task Retry_MaxRetry_EmitsSingleFailedEvent()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new FakeHandler(SyncOperationResult.TransientFailure("DB_BUSY"));
|
||||
var options = DefaultOptions();
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, options: options, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Delete, retryCount: options.MaxRetryCount);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Contains(envelope.OperationId, repo.Rejected);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
Assert.Equal("OMK-WKR-SYNC-002", evt.EventCode);
|
||||
Assert.Equal("MAX_RETRY", evt.ErrorCode);
|
||||
Assert.Equal(envelope.OperationId, evt.OperationId);
|
||||
}
|
||||
|
||||
// --------------------------- E12 : exception transitoire (détail) ----------
|
||||
[Fact]
|
||||
public async Task E12_TransientException_EmitsWarning_WithExceptionTypeAndStackTrace()
|
||||
{
|
||||
var repo = new FakeSyncRepository();
|
||||
var handler = new ThrowingHandler(new InvalidOperationException("DB indisponible (simulé)"));
|
||||
var store = new FakeDiagnosticStore();
|
||||
var processor = CreateProcessor(repo, handler, store: store, provider: Provider());
|
||||
var envelope = CreateEnvelope(SyncOperationType.Create);
|
||||
|
||||
bool handled = await processor.ProcessOperationAsync(envelope);
|
||||
|
||||
Assert.False(handled);
|
||||
(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context) call = Assert.Single(store.Calls);
|
||||
DiagnosticEventDto evt = Assert.Single(call.Batch.Events);
|
||||
|
||||
Assert.Equal("OMK-WKR-SYNC-002", evt.EventCode);
|
||||
Assert.Equal("Warning", evt.Level);
|
||||
Assert.Equal("P2", evt.Priority);
|
||||
Assert.False(evt.Success);
|
||||
Assert.Equal("UNEXPECTED", evt.ErrorCode);
|
||||
Assert.Equal("System.InvalidOperationException", evt.ExceptionType);
|
||||
Assert.NotNull(evt.StackTrace);
|
||||
Assert.Contains("DB indisponible", evt.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(envelope.OperationId, evt.OperationId);
|
||||
}
|
||||
|
||||
// ===== Fakes ===============================================================
|
||||
|
||||
private sealed class FakeDiagnosticStore : IRemoteDiagnosticIngestionStore
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context)> _calls = [];
|
||||
private readonly bool _throwOnStore;
|
||||
|
||||
public FakeDiagnosticStore(bool throwOnStore = false) => _throwOnStore = throwOnStore;
|
||||
|
||||
public IReadOnlyList<(DiagnosticBatchDto Batch, DiagnosticDeploymentContext? Context)> Calls
|
||||
{
|
||||
get { lock (_gate) { return _calls.ToArray(); } }
|
||||
}
|
||||
|
||||
public Task<int> StoreAsync(
|
||||
DiagnosticBatchDto batch,
|
||||
DiagnosticActorKey actorKey,
|
||||
string ingestionTraceId,
|
||||
DateTimeOffset receivedAtUtc,
|
||||
CancellationToken cancellationToken = default,
|
||||
DiagnosticDeploymentContext? deploymentContext = null,
|
||||
DiagnosticDeviceKey? deviceKey = null,
|
||||
string? licenseNumber = null)
|
||||
{
|
||||
if (_throwOnStore)
|
||||
{
|
||||
throw new InvalidOperationException("Store en panne (simulé).");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_calls.Add((batch, deploymentContext));
|
||||
}
|
||||
|
||||
return Task.FromResult(batch.Events.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeDeploymentContextProvider : IDiagnosticDeploymentContextProvider
|
||||
{
|
||||
public DiagnosticDeploymentContext Context { get; set; } = DiagnosticDeploymentContext.Empty;
|
||||
|
||||
public Task<DiagnosticDeploymentContext> GetAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Context);
|
||||
}
|
||||
|
||||
private sealed class FakeSyncRepository : ISyncTransferRepository
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<Guid> _succeeded = [];
|
||||
private readonly List<Guid> _rejected = [];
|
||||
|
||||
public IReadOnlyList<Guid> Succeeded { get { lock (_gate) { return _succeeded.ToArray(); } } }
|
||||
public IReadOnlyList<Guid> Rejected { get { lock (_gate) { return _rejected.ToArray(); } } }
|
||||
|
||||
public Task<bool> ExistsAsync(Guid operationId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Succeeded.Contains(operationId) || Rejected.Contains(operationId));
|
||||
|
||||
public Task<SyncOperationState> GetExistingStateAsync(Guid operationId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(SyncOperationState.Unknown);
|
||||
|
||||
public Task InsertToSrvAsync(SyncEnvelope envelope, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<SyncStatusResponse?> GetStatusAsync(Guid operationId, string deviceId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<SyncStatusResponse?>(null);
|
||||
|
||||
public Task<SyncLogEntry?> GetTerminalResultAsync(Guid operationId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<SyncLogEntry?>(null);
|
||||
|
||||
public Task WriteLogAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<int> PurgeLogsAsync(DateTime cutoffUtc, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0);
|
||||
|
||||
public Task<int> TransferToReceptionAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0);
|
||||
|
||||
public Task<IReadOnlyList<SyncEnvelope>> GetPendingOperationsAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<SyncEnvelope>>([]);
|
||||
|
||||
public Task MarkSuccessAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_gate) { _succeeded.Add(entry.OperationId); }
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task MarkRejectedAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_gate) { _rejected.Add(entry.OperationId); }
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task IncrementRetryAsync(Guid operationId, int newRetryCount, DateTime nextAttemptUtc, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class FakeHandler : ISyncOperationHandler
|
||||
{
|
||||
public FakeHandler(SyncOperationResult result) => Result = result;
|
||||
|
||||
public SyncOperationResult Result { get; set; }
|
||||
|
||||
public IReadOnlyCollection<string> SupportedEntityTypes => ["Client"];
|
||||
|
||||
public Task<SyncOperationResult> ApplyAsync(SyncOperation operation, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Result);
|
||||
}
|
||||
|
||||
private sealed class ThrowingHandler : ISyncOperationHandler
|
||||
{
|
||||
private readonly Exception _exception;
|
||||
|
||||
public ThrowingHandler(Exception exception) => _exception = exception;
|
||||
|
||||
public IReadOnlyCollection<string> SupportedEntityTypes => ["Client"];
|
||||
|
||||
public Task<SyncOperationResult> ApplyAsync(SyncOperation operation, CancellationToken cancellationToken = default)
|
||||
=> throw _exception;
|
||||
}
|
||||
|
||||
private sealed class FakeTransaction : IWebServiceTransactionContext
|
||||
{
|
||||
public IHfsqlWriteConnection Connection => throw new NotSupportedException("Le fake de transaction n'expose pas de connexion physique.");
|
||||
|
||||
public Task BeginAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task CommitAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task RollbackAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user