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,