Expose critical curation and source image APIs

This commit is contained in:
2026-03-17 22:28:50 +01:00
parent 57fe9d217d
commit 9d25304a27
11 changed files with 323 additions and 2 deletions

View File

@@ -61,6 +61,9 @@
"rollBand": "66-70", "rollBand": "66-70",
"groupKey": null, "groupKey": null,
"columnKey": "C", "columnKey": "C",
"isCurated": false,
"sourcePageNumber": 1,
"sourceImageUrl": "/api/tables/critical/slash/cells/412/source-image",
"rawCellText": "Original imported full cell text", "rawCellText": "Original imported full cell text",
"descriptionText": "Current curated prose", "descriptionText": "Current curated prose",
"rawAffixText": "+8H - 2S", "rawAffixText": "+8H - 2S",
@@ -77,6 +80,12 @@
<p class="panel-copy">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.</p> <p class="panel-copy">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.</p>
</section> </section>
<section class="panel">
<h2 class="panel-title">Cell source image</h2>
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code></p>
<p class="panel-copy">Streams the importer-generated PNG crop for the current critical cell. Returns <code>404</code> when the row has no stored crop or the artifact is missing.</p>
</section>
<section class="panel"> <section class="panel">
<h2 class="panel-title">Cell re-parse</h2> <h2 class="panel-title">Cell re-parse</h2>
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p> <p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
@@ -87,6 +96,7 @@
"rawAffixText": "+8H", "rawAffixText": "+8H",
"parseStatus": "partial", "parseStatus": "partial",
"parsedJson": "{}", "parsedJson": "{}",
"isCurated": false,
"isDescriptionOverridden": true, "isDescriptionOverridden": true,
"isRawAffixTextOverridden": false, "isRawAffixTextOverridden": false,
"areEffectsOverridden": false, "areEffectsOverridden": false,
@@ -107,6 +117,7 @@
"rawAffixText": "+10H - must parry 2 rnds", "rawAffixText": "+10H - must parry 2 rnds",
"parseStatus": "manually_curated", "parseStatus": "manually_curated",
"parsedJson": "{\"reviewed\":true}", "parsedJson": "{\"reviewed\":true}",
"isCurated": true,
"isDescriptionOverridden": true, "isDescriptionOverridden": true,
"isRawAffixTextOverridden": false, "isRawAffixTextOverridden": false,
"areEffectsOverridden": false, "areEffectsOverridden": false,

View File

@@ -15,6 +15,9 @@ public sealed class CriticalCellEditorModel
public string ColumnKey { get; set; } = string.Empty; public string ColumnKey { get; set; } = string.Empty;
public string ColumnLabel { get; set; } = string.Empty; public string ColumnLabel { get; set; } = string.Empty;
public string ColumnRole { 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 RawCellText { get; set; } = string.Empty;
public string QuickParseInput { get; set; } = string.Empty; public string QuickParseInput { get; set; } = string.Empty;
public string DescriptionText { get; set; } = string.Empty; public string DescriptionText { get; set; } = string.Empty;
@@ -43,6 +46,9 @@ public sealed class CriticalCellEditorModel
ColumnKey = response.ColumnKey, ColumnKey = response.ColumnKey,
ColumnLabel = response.ColumnLabel, ColumnLabel = response.ColumnLabel,
ColumnRole = response.ColumnRole, ColumnRole = response.ColumnRole,
IsCurated = response.IsCurated,
SourcePageNumber = response.SourcePageNumber,
SourceImageUrl = response.SourceImageUrl,
RawCellText = response.RawCellText, RawCellText = response.RawCellText,
QuickParseInput = response.QuickParseInput, QuickParseInput = response.QuickParseInput,
DescriptionText = response.DescriptionText, DescriptionText = response.DescriptionText,
@@ -68,6 +74,7 @@ public sealed class CriticalCellEditorModel
RawAffixText, RawAffixText,
ResolveParseStatus(Effects, Branches), ResolveParseStatus(Effects, Branches),
SerializeParsedEffects(Effects), SerializeParsedEffects(Effects),
IsCurated,
IsDescriptionOverridden, IsDescriptionOverridden,
IsRawAffixTextOverridden, IsRawAffixTextOverridden,
AreEffectsOverridden, AreEffectsOverridden,
@@ -101,6 +108,9 @@ public sealed class CriticalCellEditorModel
ColumnKey = ColumnKey, ColumnKey = ColumnKey,
ColumnLabel = ColumnLabel, ColumnLabel = ColumnLabel,
ColumnRole = ColumnRole, ColumnRole = ColumnRole,
IsCurated = IsCurated,
SourcePageNumber = SourcePageNumber,
SourceImageUrl = SourceImageUrl,
RawCellText = RawCellText, RawCellText = RawCellText,
QuickParseInput = QuickParseInput, QuickParseInput = QuickParseInput,
DescriptionText = DescriptionText, DescriptionText = DescriptionText,

View File

@@ -13,6 +13,9 @@ public sealed record CriticalCellEditorResponse(
string ColumnKey, string ColumnKey,
string ColumnLabel, string ColumnLabel,
string ColumnRole, string ColumnRole,
bool IsCurated,
int? SourcePageNumber,
string? SourceImageUrl,
string RawCellText, string RawCellText,
string QuickParseInput, string QuickParseInput,
string DescriptionText, string DescriptionText,

View File

@@ -9,6 +9,7 @@ public sealed record CriticalCellUpdateRequest(
string? RawAffixText, string? RawAffixText,
string ParseStatus, string ParseStatus,
string ParsedJson, string ParsedJson,
bool IsCurated,
bool IsDescriptionOverridden, bool IsDescriptionOverridden,
bool IsRawAffixTextOverridden, bool IsRawAffixTextOverridden,
bool AreEffectsOverridden, bool AreEffectsOverridden,

View File

@@ -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");
}
}

View File

@@ -110,6 +110,7 @@ public sealed record CriticalTableCellDetail(
string ColumnRole, string ColumnRole,
string? GroupKey, string? GroupKey,
string? GroupLabel, string? GroupLabel,
bool IsCurated,
string? Description, string? Description,
IReadOnlyList<CriticalEffectLookupResponse> Effects, IReadOnlyList<CriticalEffectLookupResponse> Effects,
IReadOnlyList<CriticalBranchLookupResponse> Branches); IReadOnlyList<CriticalBranchLookupResponse> Branches);

View File

@@ -9,7 +9,9 @@ using SharedParsing = RolemasterDb.CriticalParsing;
namespace RolemasterDb.App.Features; namespace RolemasterDb.App.Features;
public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbContextFactory) public sealed class LookupService(
IDbContextFactory<RolemasterDbContext> dbContextFactory,
CriticalImportArtifactLocator? artifactLocator = null)
{ {
public async Task<LookupReferenceData> GetReferenceDataAsync(CancellationToken cancellationToken = default) public async Task<LookupReferenceData> GetReferenceDataAsync(CancellationToken cancellationToken = default)
{ {
@@ -245,6 +247,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.CriticalColumn.Role, result.CriticalColumn.Role,
result.CriticalGroup?.GroupKey, result.CriticalGroup?.GroupKey,
result.CriticalGroup?.Label, result.CriticalGroup?.Label,
result.IsCurated,
result.DescriptionText, result.DescriptionText,
result.Effects result.Effects
.OrderBy(effect => effect.Id) .OrderBy(effect => effect.Id)
@@ -300,6 +303,28 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent)); return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
} }
public async Task<string?> 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<CriticalCellEditorResponse?> ReparseCriticalCellAsync( public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
string slug, string slug,
int resultId, int resultId,
@@ -363,6 +388,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.RawAffixText = NormalizeOptionalText(request.RawAffixText); result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
result.ParseStatus = request.ParseStatus.Trim(); result.ParseStatus = request.ParseStatus.Trim();
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson(); result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
result.IsCurated = request.IsCurated;
ReplaceBaseEffects(dbContext, result, request.Effects); ReplaceBaseEffects(dbContext, result, request.Effects);
ReplaceBranches(dbContext, result, request.Branches); ReplaceBranches(dbContext, result, request.Branches);
@@ -450,6 +476,9 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.CriticalColumn.ColumnKey, result.CriticalColumn.ColumnKey,
result.CriticalColumn.Label, result.CriticalColumn.Label,
result.CriticalColumn.Role, result.CriticalColumn.Role,
state.IsCurated,
result.SourcePageNumber,
CreateSourceImageUrl(result),
state.RawCellText, state.RawCellText,
state.QuickParseInput, state.QuickParseInput,
state.DescriptionText, state.DescriptionText,
@@ -596,6 +625,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.RawAffixText, result.RawAffixText,
result.ParseStatus, result.ParseStatus,
result.ParsedJson, result.ParsedJson,
result.IsCurated,
snapshot.IsDescriptionOverridden, snapshot.IsDescriptionOverridden,
snapshot.IsRawAffixTextOverridden, snapshot.IsRawAffixTextOverridden,
snapshot.AreEffectsOverridden, snapshot.AreEffectsOverridden,
@@ -620,6 +650,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.RawAffixText, result.RawAffixText,
result.ParseStatus, result.ParseStatus,
result.ParsedJson, result.ParsedJson,
result.IsCurated,
false, false,
false, false,
false, false,
@@ -645,6 +676,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
RawAffixText: content.RawAffixText, RawAffixText: content.RawAffixText,
ParseStatus: ResolveParseStatus(content.Effects, content.Branches), ParseStatus: ResolveParseStatus(content.Effects, content.Branches),
ParsedJson: SerializeParsedEffects(content.Effects), ParsedJson: SerializeParsedEffects(content.Effects),
IsCurated: false,
IsDescriptionOverridden: false, IsDescriptionOverridden: false,
IsRawAffixTextOverridden: false, IsRawAffixTextOverridden: false,
AreEffectsOverridden: false, AreEffectsOverridden: false,
@@ -663,6 +695,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText, currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
generatedState.ParseStatus, generatedState.ParseStatus,
generatedState.ParsedJson, generatedState.ParsedJson,
currentState.IsCurated,
currentState.IsDescriptionOverridden, currentState.IsDescriptionOverridden,
currentState.IsRawAffixTextOverridden, currentState.IsRawAffixTextOverridden,
currentState.AreEffectsOverridden, currentState.AreEffectsOverridden,
@@ -992,4 +1025,9 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
private static string NormalizeSlug(string value) => private static string NormalizeSlug(string value) =>
value.Trim().Replace(' ', '_').ToLowerInvariant(); 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";
} }

View File

@@ -9,6 +9,7 @@ var connectionString = builder.Configuration.GetConnectionString("RolemasterDb")
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString)); builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
builder.Services.AddScoped<LookupService>(); builder.Services.AddScoped<LookupService>();
var app = builder.Build(); 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); var result = await lookupService.GetCriticalCellEditorAsync(slug, resultId, cancellationToken);
return result is null ? Results.NotFound() : Results.Ok(result); 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) => 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); var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.CurrentState, cancellationToken);

View File

@@ -432,6 +432,7 @@ public sealed class CriticalCellReparseIntegrationTests
initialResponse.RawAffixText, initialResponse.RawAffixText,
initialResponse.ParseStatus, initialResponse.ParseStatus,
initialResponse.ParsedJson, initialResponse.ParsedJson,
initialResponse.IsCurated,
initialResponse.IsDescriptionOverridden, initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden, initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden, initialResponse.AreEffectsOverridden,
@@ -468,6 +469,7 @@ public sealed class CriticalCellReparseIntegrationTests
initialResponse.RawAffixText, initialResponse.RawAffixText,
initialResponse.ParseStatus, initialResponse.ParseStatus,
initialResponse.ParsedJson, initialResponse.ParsedJson,
initialResponse.IsCurated,
initialResponse.IsDescriptionOverridden, initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden, initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden, initialResponse.AreEffectsOverridden,
@@ -493,6 +495,7 @@ public sealed class CriticalCellReparseIntegrationTests
reopenedResponse.RawAffixText, reopenedResponse.RawAffixText,
reopenedResponse.ParseStatus, reopenedResponse.ParseStatus,
reopenedResponse.ParsedJson, reopenedResponse.ParsedJson,
reopenedResponse.IsCurated,
reopenedResponse.IsDescriptionOverridden, reopenedResponse.IsDescriptionOverridden,
reopenedResponse.IsRawAffixTextOverridden, reopenedResponse.IsRawAffixTextOverridden,
reopenedResponse.AreEffectsOverridden, reopenedResponse.AreEffectsOverridden,
@@ -524,6 +527,7 @@ public sealed class CriticalCellReparseIntegrationTests
rawAffixText, rawAffixText,
"partial", "partial",
"{}", "{}",
false,
isDescriptionOverridden, isDescriptionOverridden,
isRawAffixTextOverridden, isRawAffixTextOverridden,
areEffectsOverridden, areEffectsOverridden,
@@ -629,9 +633,16 @@ public sealed class CriticalCellReparseIntegrationTests
{ {
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db"); var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db");
File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true); File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true);
UpgradeDatabase(databasePath).GetAwaiter().GetResult();
return databasePath; return databasePath;
} }
private static async Task UpgradeDatabase(string databasePath)
{
await using var dbContext = CreateDbContext(databasePath);
await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
}
private static string GetRepositoryRoot() private static string GetRepositoryRoot()
{ {
var probe = new DirectoryInfo(AppContext.BaseDirectory); var probe = new DirectoryInfo(AppContext.BaseDirectory);

View File

@@ -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<int> 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<RolemasterDbContext>()
.UseSqlite($"Data Source={databasePath}")
.Options;
return new RolemasterDbContext(options);
}
private static IDbContextFactory<RolemasterDbContext> CreateDbContextFactory(string databasePath)
{
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
.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;
}
}

View File

@@ -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);
}