diff --git a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs b/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs index 51988864..b827bf4f 100644 --- a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs +++ b/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs @@ -4,13 +4,14 @@ using Microsoft.Extensions.Options; using SimpleModule.Database; using SimpleModule.FileStorage.Contracts; +using SimpleModule.Tests.Shared.Storage; namespace SimpleModule.FileStorage.Tests; public sealed class FileStorageServiceTests : IDisposable { private readonly FileStorageDbContext _db; - private readonly InMemoryStorageProvider _storageProvider; + private readonly InMemoryStorage _storageProvider; private readonly FileStorageService _service; public FileStorageServiceTests() @@ -32,7 +33,7 @@ public FileStorageServiceTests() _db.Database.OpenConnection(); _db.Database.EnsureCreated(); - _storageProvider = new InMemoryStorageProvider(); + _storageProvider = new InMemoryStorage(); _service = new FileStorageService( _db, _storageProvider, diff --git a/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs b/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs new file mode 100644 index 00000000..b63c92dd --- /dev/null +++ b/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using SimpleModule.BackgroundJobs.Contracts; + +namespace SimpleModule.Tests.Shared.BackgroundJobs; + +/// +/// Test fake for . Captures progress reports +/// and log messages in-memory so tests can assert on what a job did without +/// wiring up a ProgressChannel or the real DefaultJobExecutionContext. +/// +public sealed class FakeJobExecutionContext : IJobExecutionContext +{ + private readonly string? _serializedData; + private readonly List _progressReports = []; + private readonly List _logMessages = []; + + public FakeJobExecutionContext(JobId jobId, string? serializedData = null) + { + JobId = jobId; + _serializedData = serializedData; + } + + public FakeJobExecutionContext(JobId jobId, object data) + : this(jobId, JsonSerializer.Serialize(data)) { } + + public FakeJobExecutionContext() + : this(JobId.From(Guid.NewGuid()), serializedData: null) { } + + public JobId JobId { get; } + + public IReadOnlyList ProgressReports => _progressReports; + + public IReadOnlyList LogMessages => _logMessages; + + public T GetData() + { + if (string.IsNullOrEmpty(_serializedData)) + { + throw new InvalidOperationException("No data was provided for this job."); + } + + return JsonSerializer.Deserialize(_serializedData) + ?? throw new InvalidOperationException( + $"Failed to deserialize job data as {typeof(T).Name}." + ); + } + + public void ReportProgress(int percentage, string? message = null) => + _progressReports.Add(new ProgressReport(percentage, message)); + + public void Log(string message) => _logMessages.Add(message); + + public readonly record struct ProgressReport(int Percentage, string? Message); +} diff --git a/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs b/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs new file mode 100644 index 00000000..7c0117e9 --- /dev/null +++ b/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs @@ -0,0 +1,57 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using SimpleModule.Database; +using SimpleModule.Datasets; + +namespace SimpleModule.Tests.Shared.Datasets; + +/// +/// Test subclass of backed by a private +/// in-memory SQLite connection. Use the static factory +/// to obtain a ready-to-use, self-owning context; disposing the context +/// closes the underlying connection. +/// +public sealed class TestDatasetsDbContext : DatasetsDbContext +{ + private readonly SqliteConnection _connection; + + private TestDatasetsDbContext( + DbContextOptions options, + IOptions dbOptions, + SqliteConnection connection + ) + : base(options, dbOptions) + { + _connection = connection; + } + + /// + /// Creates a with a fresh in-memory + /// SQLite database. Dispose the returned context to release the + /// connection. + /// + public static TestDatasetsDbContext Create() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + var dbOptions = Options.Create( + new DatabaseOptions { DefaultConnection = "Data Source=:memory:", Provider = "Sqlite" } + ); + + var context = new TestDatasetsDbContext(options, dbOptions, connection); + context.Database.EnsureCreated(); + return context; + } + + public override void Dispose() + { + base.Dispose(); + _connection.Dispose(); + } +} diff --git a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs b/tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs similarity index 90% rename from modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs rename to tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs index ed3ba23a..867301f0 100644 --- a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs +++ b/tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs @@ -1,9 +1,13 @@ using System.Collections.Concurrent; using SimpleModule.Storage; -namespace SimpleModule.FileStorage.Tests; +namespace SimpleModule.Tests.Shared.Storage; -public sealed class InMemoryStorageProvider : IStorageProvider +/// +/// In-memory for tests. Stores files in a +/// concurrent dictionary keyed by normalized path. +/// +public sealed class InMemoryStorage : IStorageProvider { private readonly ConcurrentDictionary< string,