diff --git a/src/RolemasterDb.App/Components/Pages/Api.razor b/src/RolemasterDb.App/Components/Pages/Api.razor index ca1a313..dead3c6 100644 --- a/src/RolemasterDb.App/Components/Pages/Api.razor +++ b/src/RolemasterDb.App/Components/Pages/Api.razor @@ -61,6 +61,9 @@ "rollBand": "66-70", "groupKey": null, "columnKey": "C", + "isCurated": false, + "sourcePageNumber": 1, + "sourceImageUrl": "/api/tables/critical/slash/cells/412/source-image", "rawCellText": "Original imported full cell text", "descriptionText": "Current curated prose", "rawAffixText": "+8H - 2S", @@ -77,6 +80,12 @@
Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.
+GET /api/tables/critical/{slug}/cells/{resultId}/source-image
Streams the importer-generated PNG crop for the current critical cell. Returns 404 when the row has no stored crop or the artifact is missing.
POST /api/tables/critical/{slug}/cells/{resultId}/reparse
PUT /api/tables/critical/{slug}/cells/{resultId}
{
"rawCellText": "Corrected imported text",
- "descriptionText": "Rewritten prose after manual review",
+ "descriptionText": "Rewritten prose after manual review",
"rawAffixText": "+10H - must parry 2 rnds",
"parseStatus": "manually_curated",
"parsedJson": "{\"reviewed\":true}",
+ "isCurated": true,
"isDescriptionOverridden": true,
"isRawAffixTextOverridden": false,
"areEffectsOverridden": false,
diff --git a/src/RolemasterDb.App/Components/Shared/CriticalCellEditorModel.cs b/src/RolemasterDb.App/Components/Shared/CriticalCellEditorModel.cs
index ab02124..edc3b5e 100644
--- a/src/RolemasterDb.App/Components/Shared/CriticalCellEditorModel.cs
+++ b/src/RolemasterDb.App/Components/Shared/CriticalCellEditorModel.cs
@@ -15,6 +15,9 @@ public sealed class CriticalCellEditorModel
public string ColumnKey { get; set; } = string.Empty;
public string ColumnLabel { get; set; } = string.Empty;
public string ColumnRole { get; set; } = string.Empty;
+ public bool IsCurated { get; set; }
+ public int? SourcePageNumber { get; set; }
+ public string? SourceImageUrl { get; set; }
public string RawCellText { get; set; } = string.Empty;
public string QuickParseInput { get; set; } = string.Empty;
public string DescriptionText { get; set; } = string.Empty;
@@ -43,6 +46,9 @@ public sealed class CriticalCellEditorModel
ColumnKey = response.ColumnKey,
ColumnLabel = response.ColumnLabel,
ColumnRole = response.ColumnRole,
+ IsCurated = response.IsCurated,
+ SourcePageNumber = response.SourcePageNumber,
+ SourceImageUrl = response.SourceImageUrl,
RawCellText = response.RawCellText,
QuickParseInput = response.QuickParseInput,
DescriptionText = response.DescriptionText,
@@ -68,6 +74,7 @@ public sealed class CriticalCellEditorModel
RawAffixText,
ResolveParseStatus(Effects, Branches),
SerializeParsedEffects(Effects),
+ IsCurated,
IsDescriptionOverridden,
IsRawAffixTextOverridden,
AreEffectsOverridden,
@@ -101,6 +108,9 @@ public sealed class CriticalCellEditorModel
ColumnKey = ColumnKey,
ColumnLabel = ColumnLabel,
ColumnRole = ColumnRole,
+ IsCurated = IsCurated,
+ SourcePageNumber = SourcePageNumber,
+ SourceImageUrl = SourceImageUrl,
RawCellText = RawCellText,
QuickParseInput = QuickParseInput,
DescriptionText = DescriptionText,
diff --git a/src/RolemasterDb.App/Features/CriticalCellEditorResponse.cs b/src/RolemasterDb.App/Features/CriticalCellEditorResponse.cs
index 93de1a1..e35f4d9 100644
--- a/src/RolemasterDb.App/Features/CriticalCellEditorResponse.cs
+++ b/src/RolemasterDb.App/Features/CriticalCellEditorResponse.cs
@@ -13,6 +13,9 @@ public sealed record CriticalCellEditorResponse(
string ColumnKey,
string ColumnLabel,
string ColumnRole,
+ bool IsCurated,
+ int? SourcePageNumber,
+ string? SourceImageUrl,
string RawCellText,
string QuickParseInput,
string DescriptionText,
diff --git a/src/RolemasterDb.App/Features/CriticalCellUpdateRequest.cs b/src/RolemasterDb.App/Features/CriticalCellUpdateRequest.cs
index bedf0bf..9c6b90b 100644
--- a/src/RolemasterDb.App/Features/CriticalCellUpdateRequest.cs
+++ b/src/RolemasterDb.App/Features/CriticalCellUpdateRequest.cs
@@ -9,6 +9,7 @@ public sealed record CriticalCellUpdateRequest(
string? RawAffixText,
string ParseStatus,
string ParsedJson,
+ bool IsCurated,
bool IsDescriptionOverridden,
bool IsRawAffixTextOverridden,
bool AreEffectsOverridden,
diff --git a/src/RolemasterDb.App/Features/CriticalImportArtifactLocator.cs b/src/RolemasterDb.App/Features/CriticalImportArtifactLocator.cs
new file mode 100644
index 0000000..cc82230
--- /dev/null
+++ b/src/RolemasterDb.App/Features/CriticalImportArtifactLocator.cs
@@ -0,0 +1,44 @@
+namespace RolemasterDb.App.Features;
+
+public sealed class CriticalImportArtifactLocator
+{
+ public CriticalImportArtifactLocator(IHostEnvironment hostEnvironment)
+ {
+ ArtifactsRootPath = DiscoverArtifactsRootPath(hostEnvironment.ContentRootPath);
+ }
+
+ public string ArtifactsRootPath { get; }
+
+ public string? ResolveStoredPath(string? relativePath)
+ {
+ if (string.IsNullOrWhiteSpace(relativePath))
+ {
+ return null;
+ }
+
+ var candidate = Path.GetFullPath(Path.Combine(
+ ArtifactsRootPath,
+ relativePath.Replace('/', Path.DirectorySeparatorChar)));
+
+ return candidate.StartsWith(ArtifactsRootPath, StringComparison.OrdinalIgnoreCase)
+ ? candidate
+ : null;
+ }
+
+ private static string DiscoverArtifactsRootPath(string contentRootPath)
+ {
+ var probe = new DirectoryInfo(contentRootPath);
+
+ while (probe is not null)
+ {
+ if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
+ {
+ return Path.Combine(probe.FullName, "artifacts", "import", "critical");
+ }
+
+ probe = probe.Parent;
+ }
+
+ return Path.Combine(contentRootPath, "artifacts", "import", "critical");
+ }
+}
diff --git a/src/RolemasterDb.App/Features/LookupContracts.cs b/src/RolemasterDb.App/Features/LookupContracts.cs
index 4083375..2fd203e 100644
--- a/src/RolemasterDb.App/Features/LookupContracts.cs
+++ b/src/RolemasterDb.App/Features/LookupContracts.cs
@@ -110,6 +110,7 @@ public sealed record CriticalTableCellDetail(
string ColumnRole,
string? GroupKey,
string? GroupLabel,
+ bool IsCurated,
string? Description,
IReadOnlyList Effects,
IReadOnlyList Branches);
diff --git a/src/RolemasterDb.App/Features/LookupService.cs b/src/RolemasterDb.App/Features/LookupService.cs
index bc681e0..88d4524 100644
--- a/src/RolemasterDb.App/Features/LookupService.cs
+++ b/src/RolemasterDb.App/Features/LookupService.cs
@@ -9,7 +9,9 @@ using SharedParsing = RolemasterDb.CriticalParsing;
namespace RolemasterDb.App.Features;
-public sealed class LookupService(IDbContextFactory dbContextFactory)
+public sealed class LookupService(
+ IDbContextFactory dbContextFactory,
+ CriticalImportArtifactLocator? artifactLocator = null)
{
public async Task GetReferenceDataAsync(CancellationToken cancellationToken = default)
{
@@ -245,6 +247,7 @@ public sealed class LookupService(IDbContextFactory dbConte
result.CriticalColumn.Role,
result.CriticalGroup?.GroupKey,
result.CriticalGroup?.Label,
+ result.IsCurated,
result.DescriptionText,
result.Effects
.OrderBy(effect => effect.Id)
@@ -300,6 +303,28 @@ public sealed class LookupService(IDbContextFactory dbConte
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
}
+ public async Task GetCriticalSourceImagePathAsync(string slug, int resultId, CancellationToken cancellationToken = default)
+ {
+ if (artifactLocator is null)
+ {
+ return null;
+ }
+
+ await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
+
+ var normalizedSlug = NormalizeSlug(slug);
+ var relativePath = await dbContext.CriticalResults
+ .AsNoTracking()
+ .Where(item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug)
+ .Select(item => item.SourceImagePath)
+ .SingleOrDefaultAsync(cancellationToken);
+
+ var fullPath = artifactLocator.ResolveStoredPath(relativePath);
+ return fullPath is not null && File.Exists(fullPath)
+ ? fullPath
+ : null;
+ }
+
public async Task ReparseCriticalCellAsync(
string slug,
int resultId,
@@ -363,6 +388,7 @@ public sealed class LookupService(IDbContextFactory dbConte
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
result.ParseStatus = request.ParseStatus.Trim();
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
+ result.IsCurated = request.IsCurated;
ReplaceBaseEffects(dbContext, result, request.Effects);
ReplaceBranches(dbContext, result, request.Branches);
@@ -450,6 +476,9 @@ public sealed class LookupService(IDbContextFactory dbConte
result.CriticalColumn.ColumnKey,
result.CriticalColumn.Label,
result.CriticalColumn.Role,
+ state.IsCurated,
+ result.SourcePageNumber,
+ CreateSourceImageUrl(result),
state.RawCellText,
state.QuickParseInput,
state.DescriptionText,
@@ -596,6 +625,7 @@ public sealed class LookupService(IDbContextFactory dbConte
result.RawAffixText,
result.ParseStatus,
result.ParsedJson,
+ result.IsCurated,
snapshot.IsDescriptionOverridden,
snapshot.IsRawAffixTextOverridden,
snapshot.AreEffectsOverridden,
@@ -620,6 +650,7 @@ public sealed class LookupService(IDbContextFactory dbConte
result.RawAffixText,
result.ParseStatus,
result.ParsedJson,
+ result.IsCurated,
false,
false,
false,
@@ -645,6 +676,7 @@ public sealed class LookupService(IDbContextFactory dbConte
RawAffixText: content.RawAffixText,
ParseStatus: ResolveParseStatus(content.Effects, content.Branches),
ParsedJson: SerializeParsedEffects(content.Effects),
+ IsCurated: false,
IsDescriptionOverridden: false,
IsRawAffixTextOverridden: false,
AreEffectsOverridden: false,
@@ -663,6 +695,7 @@ public sealed class LookupService(IDbContextFactory dbConte
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
generatedState.ParseStatus,
generatedState.ParsedJson,
+ currentState.IsCurated,
currentState.IsDescriptionOverridden,
currentState.IsRawAffixTextOverridden,
currentState.AreEffectsOverridden,
@@ -992,4 +1025,9 @@ public sealed class LookupService(IDbContextFactory dbConte
private static string NormalizeSlug(string value) =>
value.Trim().Replace(' ', '_').ToLowerInvariant();
+
+ private static string? CreateSourceImageUrl(CriticalResult result) =>
+ string.IsNullOrWhiteSpace(result.SourceImagePath)
+ ? null
+ : $"/api/tables/critical/{result.CriticalTable.Slug}/cells/{result.Id}/source-image";
}
diff --git a/src/RolemasterDb.App/Program.cs b/src/RolemasterDb.App/Program.cs
index 2184265..cb7264c 100644
--- a/src/RolemasterDb.App/Program.cs
+++ b/src/RolemasterDb.App/Program.cs
@@ -9,6 +9,7 @@ var connectionString = builder.Configuration.GetConnectionString("RolemasterDb")
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddDbContextFactory(options => options.UseSqlite(connectionString));
+builder.Services.AddSingleton();
builder.Services.AddScoped();
var app = builder.Build();
@@ -40,6 +41,11 @@ api.MapGet("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, i
var result = await lookupService.GetCriticalCellEditorAsync(slug, resultId, cancellationToken);
return result is null ? Results.NotFound() : Results.Ok(result);
});
+api.MapGet("/tables/critical/{slug}/cells/{resultId:int}/source-image", async (string slug, int resultId, LookupService lookupService, CancellationToken cancellationToken) =>
+{
+ var filePath = await lookupService.GetCriticalSourceImagePathAsync(slug, resultId, cancellationToken);
+ return filePath is null ? Results.NotFound() : Results.File(filePath, "image/png");
+});
api.MapPost("/tables/critical/{slug}/cells/{resultId:int}/reparse", async (string slug, int resultId, CriticalCellReparseRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
{
var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.CurrentState, cancellationToken);
diff --git a/src/RolemasterDb.ImportTool.Tests/CriticalCellReparseIntegrationTests.cs b/src/RolemasterDb.ImportTool.Tests/CriticalCellReparseIntegrationTests.cs
index b13f637..76c2994 100644
--- a/src/RolemasterDb.ImportTool.Tests/CriticalCellReparseIntegrationTests.cs
+++ b/src/RolemasterDb.ImportTool.Tests/CriticalCellReparseIntegrationTests.cs
@@ -432,6 +432,7 @@ public sealed class CriticalCellReparseIntegrationTests
initialResponse.RawAffixText,
initialResponse.ParseStatus,
initialResponse.ParsedJson,
+ initialResponse.IsCurated,
initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden,
@@ -468,6 +469,7 @@ public sealed class CriticalCellReparseIntegrationTests
initialResponse.RawAffixText,
initialResponse.ParseStatus,
initialResponse.ParsedJson,
+ initialResponse.IsCurated,
initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden,
@@ -493,6 +495,7 @@ public sealed class CriticalCellReparseIntegrationTests
reopenedResponse.RawAffixText,
reopenedResponse.ParseStatus,
reopenedResponse.ParsedJson,
+ reopenedResponse.IsCurated,
reopenedResponse.IsDescriptionOverridden,
reopenedResponse.IsRawAffixTextOverridden,
reopenedResponse.AreEffectsOverridden,
@@ -524,6 +527,7 @@ public sealed class CriticalCellReparseIntegrationTests
rawAffixText,
"partial",
"{}",
+ false,
isDescriptionOverridden,
isRawAffixTextOverridden,
areEffectsOverridden,
@@ -629,9 +633,16 @@ public sealed class CriticalCellReparseIntegrationTests
{
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db");
File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true);
+ UpgradeDatabase(databasePath).GetAwaiter().GetResult();
return databasePath;
}
+ private static async Task UpgradeDatabase(string databasePath)
+ {
+ await using var dbContext = CreateDbContext(databasePath);
+ await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
+ }
+
private static string GetRepositoryRoot()
{
var probe = new DirectoryInfo(AppContext.BaseDirectory);
diff --git a/src/RolemasterDb.ImportTool.Tests/LookupServiceCurationIntegrationTests.cs b/src/RolemasterDb.ImportTool.Tests/LookupServiceCurationIntegrationTests.cs
new file mode 100644
index 0000000..c6080ca
--- /dev/null
+++ b/src/RolemasterDb.ImportTool.Tests/LookupServiceCurationIntegrationTests.cs
@@ -0,0 +1,184 @@
+using Microsoft.EntityFrameworkCore;
+
+using RolemasterDb.App.Data;
+using RolemasterDb.App.Domain;
+using RolemasterDb.App.Features;
+
+namespace RolemasterDb.ImportTool.Tests;
+
+public sealed class LookupServiceCurationIntegrationTests
+{
+ [Fact]
+ public async Task Lookup_service_surfaces_and_persists_curated_state_and_source_image_metadata()
+ {
+ var databasePath = CreateEmptyDatabasePath();
+ var repositoryRoot = CreateTemporaryRepositoryRoot();
+ var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
+
+ await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2);
+ WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png");
+
+ var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
+ var resultId = await GetResultIdAsync(databasePath);
+
+ var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
+ Assert.NotNull(initialResponse);
+ Assert.False(initialResponse!.IsCurated);
+ Assert.Equal(2, initialResponse.SourcePageNumber);
+ Assert.Equal($"/api/tables/critical/slash/cells/{resultId}/source-image", initialResponse.SourceImageUrl);
+
+ var updateRequest = new CriticalCellUpdateRequest(
+ initialResponse.RawCellText,
+ initialResponse.QuickParseInput,
+ initialResponse.DescriptionText,
+ initialResponse.RawAffixText,
+ initialResponse.ParseStatus,
+ initialResponse.ParsedJson,
+ true,
+ initialResponse.IsDescriptionOverridden,
+ initialResponse.IsRawAffixTextOverridden,
+ initialResponse.AreEffectsOverridden,
+ initialResponse.AreBranchesOverridden,
+ initialResponse.Effects,
+ initialResponse.Branches);
+
+ var updatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, updateRequest);
+ Assert.NotNull(updatedResponse);
+ Assert.True(updatedResponse!.IsCurated);
+
+ var tableDetail = await lookupService.GetCriticalTableAsync("slash");
+ Assert.NotNull(tableDetail);
+ Assert.Contains(tableDetail!.Cells, item => item.ResultId == resultId && item.IsCurated);
+
+ var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
+ Assert.NotNull(reopenedResponse);
+ Assert.True(reopenedResponse!.IsCurated);
+ Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
+ }
+
+ [Fact]
+ public async Task Lookup_service_resolves_source_image_paths_only_when_artifacts_exist()
+ {
+ var databasePath = CreateEmptyDatabasePath();
+ var repositoryRoot = CreateTemporaryRepositoryRoot();
+ var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
+
+ await SeedCriticalResultAsync(databasePath, "slash/cells/missing.png", 1);
+
+ var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
+ var resultId = await GetResultIdAsync(databasePath);
+
+ var missingPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId);
+ Assert.Null(missingPath);
+
+ WriteSourceImage(repositoryRoot, "slash/cells/missing.png");
+
+ var resolvedPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId);
+ Assert.NotNull(resolvedPath);
+ Assert.True(File.Exists(resolvedPath));
+ }
+
+ private static async Task SeedCriticalResultAsync(string databasePath, string sourceImagePath, int sourcePageNumber)
+ {
+ await using var dbContext = CreateDbContext(databasePath);
+ await dbContext.Database.EnsureCreatedAsync();
+ await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
+
+ var table = new CriticalTable
+ {
+ Slug = "slash",
+ DisplayName = "Slash Critical Strike Table",
+ Family = "standard",
+ SourceDocument = "Slash.pdf"
+ };
+ var column = new CriticalColumn
+ {
+ CriticalTable = table,
+ ColumnKey = "B",
+ Label = "B",
+ Role = "severity",
+ SortOrder = 2
+ };
+ var rollBand = new CriticalRollBand
+ {
+ CriticalTable = table,
+ Label = "36-40",
+ MinRoll = 36,
+ MaxRoll = 40,
+ SortOrder = 8
+ };
+
+ var result = new CriticalResult
+ {
+ CriticalTable = table,
+ CriticalColumn = column,
+ CriticalRollBand = rollBand,
+ IsCurated = false,
+ RawCellText = "Imported raw cell text",
+ DescriptionText = "Imported description",
+ ParseStatus = "partial",
+ ParsedJson = "{}",
+ SourcePageNumber = sourcePageNumber,
+ SourceImagePath = sourceImagePath,
+ SourceImageCropJson = "{\"pageNumber\":1}"
+ };
+
+ result.Effects.Add(new CriticalEffect
+ {
+ EffectCode = CriticalEffectCodes.DirectHits,
+ ValueInteger = 5,
+ IsPermanent = false,
+ SourceType = "symbol",
+ SourceText = "+5H"
+ });
+
+ dbContext.CriticalTables.Add(table);
+ dbContext.CriticalResults.Add(result);
+ await dbContext.SaveChangesAsync();
+ }
+
+ private static void WriteSourceImage(string repositoryRoot, string relativePath)
+ {
+ var fullPath = Path.Combine(repositoryRoot, "artifacts", "import", "critical", relativePath.Replace('/', Path.DirectorySeparatorChar));
+ Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
+ File.WriteAllBytes(fullPath, [137, 80, 78, 71]);
+ }
+
+ private static async Task GetResultIdAsync(string databasePath)
+ {
+ await using var dbContext = CreateDbContext(databasePath);
+ return await dbContext.CriticalResults
+ .Where(item => item.CriticalTable.Slug == "slash")
+ .Select(item => item.Id)
+ .SingleAsync();
+ }
+
+ private static RolemasterDbContext CreateDbContext(string databasePath)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite($"Data Source={databasePath}")
+ .Options;
+
+ return new RolemasterDbContext(options);
+ }
+
+ private static IDbContextFactory CreateDbContextFactory(string databasePath)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite($"Data Source={databasePath}")
+ .Options;
+
+ return new TestRolemasterDbContextFactory(options);
+ }
+
+ private static string CreateEmptyDatabasePath() =>
+ Path.Combine(Path.GetTempPath(), $"lookup-service-curation-{Guid.NewGuid():N}.db");
+
+ private static string CreateTemporaryRepositoryRoot()
+ {
+ var repositoryRoot = Path.Combine(Path.GetTempPath(), $"rolemaster-repo-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"));
+ File.WriteAllText(Path.Combine(repositoryRoot, "RolemasterDB.slnx"), string.Empty);
+ return repositoryRoot;
+ }
+}
diff --git a/src/RolemasterDb.ImportTool.Tests/TestHostEnvironment.cs b/src/RolemasterDb.ImportTool.Tests/TestHostEnvironment.cs
new file mode 100644
index 0000000..127add9
--- /dev/null
+++ b/src/RolemasterDb.ImportTool.Tests/TestHostEnvironment.cs
@@ -0,0 +1,12 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
+
+namespace RolemasterDb.ImportTool.Tests;
+
+internal sealed class TestHostEnvironment(string contentRootPath) : IHostEnvironment
+{
+ public string EnvironmentName { get; set; } = Environments.Development;
+ public string ApplicationName { get; set; } = "RolemasterDb.Tests";
+ public string ContentRootPath { get; set; } = contentRootPath;
+ public IFileProvider ContentRootFileProvider { get; set; } = new PhysicalFileProvider(contentRootPath);
+}