using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.StoreAndForward.Tests; /// /// WP-9: Tests for SQLite persistence layer. /// Uses in-memory SQLite with a kept-alive connection for test isolation. /// public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable { private readonly SqliteConnection _keepAlive; private readonly StoreAndForwardStorage _storage; private readonly string _dbName; public StoreAndForwardStorageTests() { _dbName = $"StorageTests_{Guid.NewGuid():N}"; var connStr = $"Data Source={_dbName};Mode=Memory;Cache=Shared"; // Keep one connection alive so the in-memory DB persists _keepAlive = new SqliteConnection(connStr); _keepAlive.Open(); _storage = new StoreAndForwardStorage(connStr, NullLogger.Instance); } public async Task InitializeAsync() => await _storage.InitializeAsync(); public Task DisposeAsync() => Task.CompletedTask; public void Dispose() { _keepAlive.Dispose(); } [Fact] public async Task EnqueueAsync_StoresMessage() { var message = CreateMessage("msg1", StoreAndForwardCategory.ExternalSystem); await _storage.EnqueueAsync(message); var retrieved = await _storage.GetMessageByIdAsync("msg1"); Assert.NotNull(retrieved); Assert.Equal("msg1", retrieved!.Id); Assert.Equal(StoreAndForwardCategory.ExternalSystem, retrieved.Category); Assert.Equal("target1", retrieved.Target); } [Fact] public async Task EnqueueAsync_AllCategories() { await _storage.EnqueueAsync(CreateMessage("es1", StoreAndForwardCategory.ExternalSystem)); await _storage.EnqueueAsync(CreateMessage("n1", StoreAndForwardCategory.Notification)); await _storage.EnqueueAsync(CreateMessage("db1", StoreAndForwardCategory.CachedDbWrite)); var es = await _storage.GetMessageByIdAsync("es1"); var n = await _storage.GetMessageByIdAsync("n1"); var db = await _storage.GetMessageByIdAsync("db1"); Assert.Equal(StoreAndForwardCategory.ExternalSystem, es!.Category); Assert.Equal(StoreAndForwardCategory.Notification, n!.Category); Assert.Equal(StoreAndForwardCategory.CachedDbWrite, db!.Category); } [Fact] public async Task RemoveMessageAsync_RemovesSuccessfully() { await _storage.EnqueueAsync(CreateMessage("rm1", StoreAndForwardCategory.ExternalSystem)); await _storage.RemoveMessageAsync("rm1"); var retrieved = await _storage.GetMessageByIdAsync("rm1"); Assert.Null(retrieved); } [Fact] public async Task UpdateMessageAsync_UpdatesFields() { var message = CreateMessage("upd1", StoreAndForwardCategory.ExternalSystem); await _storage.EnqueueAsync(message); message.RetryCount = 5; message.LastAttemptAt = DateTimeOffset.UtcNow; message.Status = StoreAndForwardMessageStatus.Parked; message.LastError = "Connection refused"; await _storage.UpdateMessageAsync(message); var retrieved = await _storage.GetMessageByIdAsync("upd1"); Assert.Equal(5, retrieved!.RetryCount); Assert.Equal(StoreAndForwardMessageStatus.Parked, retrieved.Status); Assert.Equal("Connection refused", retrieved.LastError); } [Fact] public async Task GetMessagesForRetryAsync_ReturnsOnlyPendingMessages() { var pending = CreateMessage("pend1", StoreAndForwardCategory.ExternalSystem); pending.Status = StoreAndForwardMessageStatus.Pending; await _storage.EnqueueAsync(pending); var parked = CreateMessage("park1", StoreAndForwardCategory.ExternalSystem); parked.Status = StoreAndForwardMessageStatus.Parked; await _storage.EnqueueAsync(parked); await _storage.UpdateMessageAsync(parked); var forRetry = await _storage.GetMessagesForRetryAsync(); Assert.All(forRetry, m => Assert.Equal(StoreAndForwardMessageStatus.Pending, m.Status)); } [Fact] public async Task GetParkedMessagesAsync_ReturnsParkedOnly() { var msg = CreateMessage("prk1", StoreAndForwardCategory.Notification); msg.Status = StoreAndForwardMessageStatus.Parked; await _storage.EnqueueAsync(msg); await _storage.UpdateMessageAsync(msg); var (messages, total) = await _storage.GetParkedMessagesAsync(); Assert.True(total > 0); Assert.All(messages, m => Assert.Equal(StoreAndForwardMessageStatus.Parked, m.Status)); } [Fact] public async Task RetryParkedMessageAsync_MovesToPending() { var msg = CreateMessage("retry1", StoreAndForwardCategory.ExternalSystem); msg.Status = StoreAndForwardMessageStatus.Parked; msg.RetryCount = 10; await _storage.EnqueueAsync(msg); await _storage.UpdateMessageAsync(msg); var success = await _storage.RetryParkedMessageAsync("retry1"); Assert.True(success); var retrieved = await _storage.GetMessageByIdAsync("retry1"); Assert.Equal(StoreAndForwardMessageStatus.Pending, retrieved!.Status); Assert.Equal(0, retrieved.RetryCount); } [Fact] public async Task DiscardParkedMessageAsync_RemovesMessage() { var msg = CreateMessage("disc1", StoreAndForwardCategory.ExternalSystem); msg.Status = StoreAndForwardMessageStatus.Parked; await _storage.EnqueueAsync(msg); await _storage.UpdateMessageAsync(msg); var success = await _storage.DiscardParkedMessageAsync("disc1"); Assert.True(success); var retrieved = await _storage.GetMessageByIdAsync("disc1"); Assert.Null(retrieved); } [Fact] public async Task GetBufferDepthByCategoryAsync_ReturnsCorrectCounts() { await _storage.EnqueueAsync(CreateMessage("bd1", StoreAndForwardCategory.ExternalSystem)); await _storage.EnqueueAsync(CreateMessage("bd2", StoreAndForwardCategory.ExternalSystem)); await _storage.EnqueueAsync(CreateMessage("bd3", StoreAndForwardCategory.Notification)); var depth = await _storage.GetBufferDepthByCategoryAsync(); Assert.True(depth.GetValueOrDefault(StoreAndForwardCategory.ExternalSystem) >= 2); } [Fact] public async Task GetMessageCountByOriginInstanceAsync_ReturnsCount() { var msg1 = CreateMessage("oi1", StoreAndForwardCategory.ExternalSystem); msg1.OriginInstanceName = "Pump1"; await _storage.EnqueueAsync(msg1); var msg2 = CreateMessage("oi2", StoreAndForwardCategory.Notification); msg2.OriginInstanceName = "Pump1"; await _storage.EnqueueAsync(msg2); var count = await _storage.GetMessageCountByOriginInstanceAsync("Pump1"); Assert.Equal(2, count); } [Fact] public async Task GetParkedMessagesAsync_Pagination() { for (int i = 0; i < 5; i++) { var msg = CreateMessage($"page{i}", StoreAndForwardCategory.ExternalSystem); msg.Status = StoreAndForwardMessageStatus.Parked; await _storage.EnqueueAsync(msg); await _storage.UpdateMessageAsync(msg); } var (page1, total) = await _storage.GetParkedMessagesAsync(pageNumber: 1, pageSize: 2); Assert.Equal(2, page1.Count); Assert.True(total >= 5); var (page2, _) = await _storage.GetParkedMessagesAsync(pageNumber: 2, pageSize: 2); Assert.Equal(2, page2.Count); } [Fact] public async Task GetMessageCountByStatusAsync_ReturnsAccurateCount() { var msg = CreateMessage("cnt1", StoreAndForwardCategory.ExternalSystem); await _storage.EnqueueAsync(msg); var count = await _storage.GetMessageCountByStatusAsync(StoreAndForwardMessageStatus.Pending); Assert.True(count >= 1); } private static StoreAndForwardMessage CreateMessage(string id, StoreAndForwardCategory category) { return new StoreAndForwardMessage { Id = id, Category = category, Target = "target1", PayloadJson = """{"method":"Test","args":{}}""", RetryCount = 0, MaxRetries = 50, RetryIntervalMs = 30000, CreatedAt = DateTimeOffset.UtcNow, Status = StoreAndForwardMessageStatus.Pending }; } }