using FieldLogger.Sync; namespace FieldLogger.Sync.Tests; /// /// In-memory stand-in for the broker. Records published payloads, lets a test toggle /// connectivity, force publish failures, and inject application-level acks — so the engine's /// durability/ack/backoff behaviour is exercised without MQTTnet or a live broker. /// public sealed class FakeMqttTransport : IMqttTransport { public bool IsConnected { get; set; } = true; public bool FailNextPublish { get; set; } public List<(string Topic, byte[] Payload)> Published { get; } = new(); public event Func? AckReceived; public Task ConnectAsync(string ackTopic, CancellationToken ct = default) { IsConnected = true; return Task.CompletedTask; } public Task DisconnectAsync() { IsConnected = false; return Task.CompletedTask; } public Task PublishAsync(string topic, byte[] payload, string schemaVersion, CancellationToken ct = default) { if (!IsConnected) throw new InvalidOperationException("not connected"); if (FailNextPublish) { FailNextPublish = false; throw new InvalidOperationException("simulated broker publish failure"); } Published.Add((topic, payload)); return Task.CompletedTask; } /// Simulate the cloud emitting an application-level ack for these pointIds. public async Task InjectAckAsync(params AckItem[] items) { if (AckReceived is { } handlers) { var batch = new AckBatch { Results = items.ToList() }; foreach (Func handler in handlers.GetInvocationList()) await handler(batch); } } public Task InjectAcceptAsync(string pointId) => InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.ACCEPTED }); public Task InjectDuplicateAsync(string pointId) => InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.DUPLICATE }); public Task InjectRejectAsync(string pointId, string reasonCode) => InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.REJECTED, ReasonCode = reasonCode }); }