Refactor stored procedure and update project structure
Playwright Tests / test (push) Has been cancelled

- Modified the stored procedure `fds__admin_getReportCatalog.sql` to use the correct schema for `all_objects`.
- Added new folders and projects for `eRechnungLib` in the solution file `Fuchs_Intranet.slnx`, including validation and test projects.
- Updated submodule reference for `OCORE`.
- Added new submodule `eRechnungLib` with initial commit.
This commit is contained in:
2026-07-06 00:01:35 +02:00
parent daac828c19
commit 4abf81cd7d
27 changed files with 1544 additions and 94 deletions
+1
View File
@@ -28,6 +28,7 @@
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\MT940Parser\MT940Parser\MT940Parser.csproj" />
<ProjectReference Include="..\CAMTParser\CAMTParser.csproj" />
<ProjectReference Include="..\eRechnungLib\src\eRechnungLib\eRechnungLib.csproj" />
</ItemGroup>
<ItemGroup>
+370
View File
@@ -0,0 +1,370 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using eRechnungLib;
using eRechnungLib.Model;
using eRechnungLib.Model.CodeLists;
using eRechnungLib.Profiles;
using Fuchs.intranet;
using Fuchs.Services;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using Microsoft.Extensions.Configuration;
using PdfSharp.Pdf.IO;
using Xunit;
namespace Fuchs.Tests;
/// <summary>
/// Exercises the invoice PDF pipeline end to end:
/// 1. a visual invoice PDF is produced with PdfSharp/MigraDoc,
/// 2. it is rasterised to preview images via Spire (the licensed path used by <c>sprep</c>),
/// 3. it is turned into a formally valid eRechnung (ZUGFeRD/Factur-X hybrid + XRechnung XML)
/// via eRechnungLib — the direction the project is moving in (all invoices as eRechnung).
/// Both an intentionally succeeding and an intentionally failing conversion path are covered.
/// </summary>
public class PdfPipelineTests
{
// ── Stage 1 helper: a "dummy" visual invoice PDF built purely with PdfSharp/MigraDoc ──
private static byte[] BuildDummyPdfWithPdfSharp()
{
// Same font resolver the production render path installs (PdfSharp 6 no longer
// resolves system fonts on its own).
if (PdfSharp.Fonts.GlobalFontSettings.FontResolver is null ||
PdfSharp.Fonts.GlobalFontSettings.FontResolver.GetType() != typeof(OCORE_web_pdf.pdf.OCOREFontResolver))
{
PdfSharp.Fonts.GlobalFontSettings.FontResolver = new OCORE_web_pdf.pdf.OCOREFontResolver();
}
var doc = new Document();
doc.Info.Title = "Dummy Rechnung";
var normal = doc.Styles["Normal"]!;
normal.Font.Name = "Arial";
var sec = doc.AddSection();
var title = sec.AddParagraph("Rechnung Nr. RE-2026-0001");
title.Format.Font.Size = 14;
title.Format.Font.Bold = true;
sec.AddParagraph("Position 1: Beratungsleistung — 200,00 EUR netto");
sec.AddParagraph("Position 2: Entwicklung — 500,00 EUR netto");
var renderer = new PdfDocumentRenderer { Document = doc };
renderer.RenderDocument();
using var ms = new MemoryStream();
renderer.PdfDocument.Save(ms, closeStream: false);
return ms.ToArray();
}
// ── Stage 3 helper: a minimal but EN 16931-complete domestic invoice model ──
private static Invoice BuildValidInvoice(string number = "RE-2026-0001")
{
var seller = new TradeParty
{
Name = "Sebastian Fuchs Bad und Heizung GmbH & Co. KG",
Address = new PostalAddress
{
Line1 = "Germaniastraße 15",
City = "Düsseldorf",
PostalCode = "40223",
Country = CountryCode.Germany,
},
Contact = new TradeContact { Name = "Sebastian Fuchs", Email = "info@sanitaerfuchs.de", Telephone = "0211 3107222" },
ElectronicAddress = new Identifier("DE286366012", "0204"),
};
seller.TaxRegistrations.Add(new TaxRegistration("DE286366012", TaxRegistrationScheme.Vat));
var buyer = new TradeParty
{
Name = "Beispiel Kunde AG",
Address = new PostalAddress
{
Line1 = "Kundenweg 2",
City = "München",
PostalCode = "80331",
Country = CountryCode.Germany,
},
};
var invoice = new Invoice
{
InvoiceNumber = number,
IssueDate = new DateOnly(2026, 6, 1),
CurrencyCode = CurrencyCode.Eur,
BuyerReference = "04011000-12345-34",
Seller = seller,
Buyer = buyer,
Payment = new PaymentInstructions
{
MeansCode = PaymentMeansCode.SepaCreditTransfer,
RemittanceInformation = number,
},
PaymentTerms = new PaymentTerms
{
Description = "Zahlbar innerhalb von 14 Tagen netto.",
DueDate = new DateOnly(2026, 6, 15),
},
};
invoice.Payment.CreditTransfers.Add(new CreditTransferAccount
{
AccountId = "DE52301502000002091478",
AccountName = seller.Name,
BankId = "WELADED1KSD",
});
invoice.Lines.Add(new InvoiceLine
{
Id = "1",
Quantity = 1m,
UnitCode = UnitCode.One,
NetPrice = 200m,
VatCategory = VatCategoryCode.StandardRate,
VatRate = 19m,
Item = new TradeItem { Name = "Beratungsleistung", Description = "Beratung nach Aufwand" },
});
invoice.Lines.Add(new InvoiceLine
{
Id = "2",
Quantity = 1m,
UnitCode = UnitCode.One,
NetPrice = 500m,
VatCategory = VatCategoryCode.StandardRate,
VatRate = 19m,
Item = new TradeItem { Name = "Entwicklung", SellerItemId = "DEV-01" },
});
InvoiceCalculator.Recalculate(invoice);
return invoice;
}
// ── Stage 1 ───────────────────────────────────────────────────────────────
[Fact]
public void Stage1_PdfSharp_produces_a_valid_pdf()
{
byte[] pdf = BuildDummyPdfWithPdfSharp();
Assert.True(pdf.Length > 1000);
Assert.StartsWith("%PDF", Encoding.ASCII.GetString(pdf, 0, 4));
}
// ── Stage 2 (Spire rasterisation — the sprep preview path) ─────────────────
[Fact]
public async Task Stage2_Spire_rasterises_the_pdf_to_preview_images()
{
FuchsPdf.SetLicense();
byte[] pdf = BuildDummyPdfWithPdfSharp();
var images = await FuchsPdf.BytesToImageCollection(pdf);
Assert.True(images.TotalPages >= 1);
Assert.NotEmpty(images.ImgB64Array);
Assert.All(images.ImgB64Array, b64 => Assert.False(string.IsNullOrWhiteSpace(b64)));
}
// ── Stage 3 (eRechnung XML) ────────────────────────────────────────────────
[Theory]
[InlineData(XRechnungSyntax.Ubl)]
[InlineData(XRechnungSyntax.Cii)]
public void Stage3_eRechnung_XRechnung_is_valid(XRechnungSyntax syntax)
{
var result = EInvoice.CreateInvoice(BuildValidInvoice())
.ToXRechnung(syntax, XRechnungVersion.V4_0);
Assert.True(result.Success);
Assert.True(result.Validation.IsValid, result.Validation.ToString());
Assert.Contains("RE-2026-0001", Encoding.UTF8.GetString(result.Value!));
}
// ── Stage 3 (ZUGFeRD hybrid embedded into the PdfSharp visual PDF) ─────────
[Fact]
public void Stage3_eRechnung_Zugferd_embeds_xml_into_supplied_visual_pdf()
{
byte[] visualPdf = BuildDummyPdfWithPdfSharp();
var result = EInvoice.CreateInvoice(BuildValidInvoice())
.ToZugferd(ZugferdProfile.EN16931, visualPdf);
Assert.True(result.Success);
Assert.StartsWith("%PDF", Encoding.ASCII.GetString(result.Value!, 0, 4));
using var ms = new MemoryStream(result.Value!);
var pdfDoc = PdfReader.Open(ms, PdfDocumentOpenMode.Import);
// Factur-X associated-file array must be present on the catalog.
Assert.NotNull(pdfDoc.Internals.Catalog.Elements.GetArray("/AF"));
}
// ── Full chain: PdfSharp → Spire images → eRechnung hybrid ─────────────────
[Fact]
public async Task FullChain_pdfsharp_spire_eRechnung()
{
FuchsPdf.SetLicense();
// 1. Visual PDF via PdfSharp.
byte[] visualPdf = BuildDummyPdfWithPdfSharp();
Assert.StartsWith("%PDF", Encoding.ASCII.GetString(visualPdf, 0, 4));
// 2. Preview images via Spire.
var images = await FuchsPdf.BytesToImageCollection(visualPdf);
Assert.True(images.TotalPages >= 1);
Assert.NotEmpty(images.ImgB64Array);
// 3. eRechnung (ZUGFeRD/Factur-X) embedding the CII XML into the visual PDF.
var hybrid = EInvoice.CreateInvoice(BuildValidInvoice()).ToZugferd(ZugferdProfile.EN16931, visualPdf);
Assert.True(hybrid.Success);
Assert.True(hybrid.Validation.IsValid, hybrid.Validation.ToString());
using var ms = new MemoryStream(hybrid.Value!);
var pdfDoc = PdfReader.Open(ms, PdfDocumentOpenMode.Import);
var names = pdfDoc.Internals.Catalog.Elements.GetDictionary("/Names");
var embeddedFiles = names!.Elements.GetDictionary("/EmbeddedFiles");
var nameArray = embeddedFiles!.Elements.GetArray("/Names");
Assert.Contains(nameArray!.Elements, e => e.ToString()!.Contains("factur-x.xml"));
}
// ── Intentionally failing conversion path ──────────────────────────────────
[Fact]
public void eRechnung_strict_validation_withholds_output_on_invalid_invoice()
{
var invoice = BuildValidInvoice("RE-2026-0009");
invoice.BuyerReference = null; // violates BR-DE-15 for XRechnung
var result = EInvoice.CreateInvoice(invoice)
.ToXRechnung(XRechnungSyntax.Ubl, XRechnungVersion.V4_0,
new ConversionOptions { StrictValidation = true });
Assert.False(result.Success);
Assert.Null(result.Value);
Assert.Contains(result.Validation.Errors, m => m.RuleId == "BR-DE-15");
}
// ── Spire license selection (managed secret vs embedded fallback) ──────────
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ResolveLicenseKey_falls_back_to_embedded_when_secret_absent(string? provided)
{
string key = FuchsPdf.ResolveLicenseKey(provided);
Assert.False(string.IsNullOrWhiteSpace(key));
Assert.True(key.Length > 100); // the embedded key, not the (empty) input
}
[Fact]
public void ResolveLicenseKey_uses_managed_secret_when_present()
{
const string secret = "MANAGED-SECRET-LICENSE-VALUE";
Assert.Equal(secret, FuchsPdf.ResolveLicenseKey(secret));
}
[Fact]
public void LicenseConfigKey_matches_the_managed_secret_name_in_appsettings()
{
// The Key Vault secret is "fuchs--SpirePdf-License"; the secret-management layer strips
// the app prefix, splits "--" into ":" and maps "-" to "_" per segment. So the managed
// key "SpirePdf-License" must surface under the config key the service reads.
const string managedSecretName = "SpirePdf-License";
string expectedConfigKey = string.Join(':',
managedSecretName.Split("--").Select(seg => seg.Replace("-", "_")));
Assert.Equal(FuchsPdfService.LicenseConfigKey, expectedConfigKey);
// And that managed secret is actually registered in the real appsettings.json.
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();
var managedKeys = config.GetSection("SecretManagement:ManagedSecretKeys").Get<string[]>() ?? [];
Assert.Contains(managedSecretName, managedKeys);
}
[Theory]
[InlineData("SpirePdf_License")] // managed-secret mapping (fuchs--SpirePdf-License → '-' to '_')
[InlineData("SpirePdf-License")] // verbatim, e.g. appsettings.Development.json
[InlineData("SpirePdf:License")] // ':'-hierarchy variant
[InlineData("fuchs:SpirePdf-License")]
public void ResolveLicenseFromConfiguration_FindsLicenseUnderEachKnownKeyVariant(string key)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { [key] = "the-license-value" })
.Build();
string? value = FuchsPdfService.ResolveLicenseFromConfiguration(config, out string? matchedKey);
Assert.Equal("the-license-value", value);
Assert.Equal(key, matchedKey);
}
[Fact]
public void ResolveLicenseFromConfiguration_ReturnsNull_WhenNoCandidateHasAValue()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SpirePdf_License"] = " ", // whitespace-only is treated as absent
["Unrelated:Key"] = "x"
})
.Build();
string? value = FuchsPdfService.ResolveLicenseFromConfiguration(config, out string? matchedKey);
Assert.Null(value);
Assert.Null(matchedKey);
}
[Fact]
public void ResolveLicenseFromConfiguration_ReturnsNull_WhenValueIsUnloadedManagedSecretPlaceholder()
{
// appsettings.json ships "SpirePdf_License": "MANAGED_BY_KEYVAULT" so the key always
// exists; until Key Vault/cache overrides it, the literal must be treated as "no license"
// so the embedded fallback is used rather than applying the placeholder as a bogus key.
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[FuchsPdfService.LicenseConfigKey] = FuchsPdfService.UnloadedSecretPlaceholder
})
.Build();
string? value = FuchsPdfService.ResolveLicenseFromConfiguration(config, out string? matchedKey);
Assert.Null(value);
Assert.Null(matchedKey);
}
[Fact]
public void ResolveLicenseFromConfiguration_SkipsPlaceholder_AndReturnsRealValueFromAnotherCandidate()
{
// The canonical key still carries the unresolved placeholder while a real license was
// supplied verbatim (e.g. appsettings.Development.json) under a different candidate key.
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[FuchsPdfService.LicenseConfigKey] = FuchsPdfService.UnloadedSecretPlaceholder,
["SpirePdf-License"] = "the-real-license"
})
.Build();
string? value = FuchsPdfService.ResolveLicenseFromConfiguration(config, out string? matchedKey);
Assert.Equal("the-real-license", value);
Assert.Equal("SpirePdf-License", matchedKey);
}
[Fact]
public void SpireLikeConfigKeys_ReportsSpireRelatedKeysForDiagnostics()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["fuchs:SpirePdf-License"] = "value",
["Other:Setting"] = "value"
})
.Build();
var keys = FuchsPdfService.SpireLikeConfigKeys(config).ToArray();
Assert.Contains("fuchs:SpirePdf-License", keys);
Assert.DoesNotContain("Other:Setting", keys);
}
}
+308
View File
@@ -0,0 +1,308 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Fuchs.intranet;
using Fuchs.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace Fuchs.Tests;
public class StartupSelfTestServiceTests
{
private static StartupSelfTestSettings CreateSettings(
bool enabled,
bool checkKeyVault = true,
bool checkDatabase = true,
bool checkMfr = true,
bool sendStartupEmail = false,
string startupRecipient = "",
bool checkPdfLicense = false) => new()
{
Enabled = enabled,
CheckKeyVault = checkKeyVault,
CheckDatabase = checkDatabase,
CheckMfr = checkMfr,
CheckPdfLicense = checkPdfLicense,
SendStartupEmail = sendStartupEmail,
StartupEmailRecipient = startupRecipient,
StartupEmailRecipientName = "Monitor"
};
private static IConfiguration CreateConfiguration() =>
new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["SecretManagement:AppName"] = "fuchs",
["SecretManagement:ManagedSecretKeys:0"] = "Fuchs--Mailer--Token"
}).Build();
[Fact]
public async Task ExecuteAsync_Disabled_DoesNotRunAnyChecks()
{
using var service = new TestableStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
CreateConfiguration(),
Options.Create(CreateSettings(enabled: false)),
NullLogger<StartupSelfTestService>.Instance)
{
KeyVaultResult = true,
DatabaseResult = true,
MfrResult = true,
MailerResult = true
};
await service.RunForTestAsync(CancellationToken.None);
Assert.Equal(0, service.KeyVaultCalls);
Assert.Equal(0, service.DatabaseCalls);
Assert.Equal(0, service.MfrCalls);
Assert.Equal(0, service.MailerCalls);
}
[Fact]
public async Task ExecuteAsync_EnabledWithAllChecks_CallsAllProbes()
{
using var service = new TestableStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
CreateConfiguration(),
Options.Create(CreateSettings(enabled: true, checkKeyVault: true, checkDatabase: true, checkMfr: true, sendStartupEmail: true, startupRecipient: "ops@example.test", checkPdfLicense: true)),
NullLogger<StartupSelfTestService>.Instance)
{
KeyVaultResult = true,
DatabaseResult = true,
MfrResult = true,
MailerResult = true,
PdfLicenseResult = true
};
await service.RunForTestAsync(CancellationToken.None);
Assert.Equal(1, service.KeyVaultCalls);
Assert.Equal(1, service.DatabaseCalls);
Assert.Equal(1, service.MfrCalls);
Assert.Equal(1, service.MailerCalls);
Assert.Equal(1, service.PdfLicenseCalls);
}
[Fact]
public async Task ExecuteAsync_PdfLicenseCheckDisabled_DoesNotProbePdfLicense()
{
using var service = new TestableStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
CreateConfiguration(),
Options.Create(CreateSettings(enabled: true, checkKeyVault: false, checkDatabase: false, checkMfr: false, checkPdfLicense: false)),
NullLogger<StartupSelfTestService>.Instance)
{
PdfLicenseResult = true
};
await service.RunForTestAsync(CancellationToken.None);
Assert.Equal(0, service.PdfLicenseCalls);
}
[Fact]
public async Task ProbePdfLicenseAsync_MissingLicenseString_ReturnsFalse()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
using var service = new ProbeExposingStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
config,
Options.Create(CreateSettings(enabled: true, checkPdfLicense: true)),
NullLogger<StartupSelfTestService>.Instance);
bool ok = await service.InvokeProbePdfLicenseAsync(CancellationToken.None);
Assert.False(ok);
}
[Fact]
public async Task ProbePdfLicenseAsync_EmptyLicenseString_ReturnsFalse()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[FuchsPdfService.LicenseConfigKey] = " "
})
.Build();
using var service = new ProbeExposingStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
config,
Options.Create(CreateSettings(enabled: true, checkPdfLicense: true)),
NullLogger<StartupSelfTestService>.Instance);
bool ok = await service.InvokeProbePdfLicenseAsync(CancellationToken.None);
Assert.False(ok);
}
[Fact]
public async Task ProbePdfLicenseAsync_InvalidLicenseString_ReportsUnlicensed()
{
// A syntactically-present but invalid key leaves Spire.PDF in evaluation mode, which the
// probe must detect (the evaluation watermark appears on the rendered document).
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[FuchsPdfService.LicenseConfigKey] = "not-a-valid-spire-license-key"
})
.Build();
using var service = new ProbeExposingStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
config,
Options.Create(CreateSettings(enabled: true, checkPdfLicense: true)),
NullLogger<StartupSelfTestService>.Instance);
try
{
bool ok = await service.InvokeProbePdfLicenseAsync(CancellationToken.None);
Assert.False(ok);
}
finally
{
// Restore the embedded (valid-format) key so other Spire-using tests aren't left
// with a malformed license that makes Spire throw on save.
FuchsPdf.SetLicense();
}
}
[Fact]
public void SpirePdfIsLicensed_InEvaluationMode_ReturnsFalse()
{
// The embedded fallback key does not license current Spire.PDF, so Spire runs as the
// evaluation edition and stamps a watermark, which the detection reports as unlicensed.
FuchsPdf.SetLicense();
Assert.False(StartupSelfTestService.SpirePdfIsLicensed());
}
[Fact]
public async Task ExecuteAsync_EnabledWithMailerOnly_CallsOnlyMailerCheck()
{
using var service = new TestableStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
CreateConfiguration(),
Options.Create(CreateSettings(enabled: true, checkKeyVault: false, checkDatabase: false, checkMfr: false, sendStartupEmail: true, startupRecipient: "ops@example.test")),
NullLogger<StartupSelfTestService>.Instance)
{
KeyVaultResult = true,
DatabaseResult = true,
MfrResult = true,
MailerResult = false
};
await service.RunForTestAsync(CancellationToken.None);
Assert.Equal(0, service.KeyVaultCalls);
Assert.Equal(0, service.DatabaseCalls);
Assert.Equal(0, service.MfrCalls);
Assert.Equal(1, service.MailerCalls);
}
[Fact]
public async Task ExecuteAsync_ProbeThrows_ServiceDoesNotThrowAndContinuesRemainingChecks()
{
using var service = new TestableStartupSelfTestService(
new ServiceCollection().BuildServiceProvider(),
CreateConfiguration(),
Options.Create(CreateSettings(enabled: true, checkKeyVault: true, checkDatabase: true, checkMfr: true, sendStartupEmail: true, startupRecipient: "ops@example.test")),
NullLogger<StartupSelfTestService>.Instance)
{
KeyVaultException = new InvalidOperationException("probe failed"),
DatabaseResult = true,
MfrResult = true,
MailerResult = true
};
var exception = await Record.ExceptionAsync(async () =>
{
await service.RunForTestAsync(CancellationToken.None);
});
Assert.Null(exception);
Assert.Equal(1, service.KeyVaultCalls);
Assert.Equal(1, service.DatabaseCalls);
Assert.Equal(1, service.MfrCalls);
Assert.Equal(1, service.MailerCalls);
}
private sealed class TestableStartupSelfTestService : StartupSelfTestService
{
public int KeyVaultCalls { get; private set; }
public int DatabaseCalls { get; private set; }
public int MfrCalls { get; private set; }
public int MailerCalls { get; private set; }
public int PdfLicenseCalls { get; private set; }
public bool KeyVaultResult { get; set; }
public bool DatabaseResult { get; set; }
public bool MfrResult { get; set; }
public bool MailerResult { get; set; }
public bool PdfLicenseResult { get; set; }
public Exception? KeyVaultException { get; set; }
public TestableStartupSelfTestService(
IServiceProvider serviceProvider,
IConfiguration configuration,
IOptions<StartupSelfTestSettings> settings,
Microsoft.Extensions.Logging.ILogger<StartupSelfTestService> logger)
: base(serviceProvider, configuration, settings, logger)
{
}
protected override Task<bool> ProbeKeyVaultAsync(CancellationToken cancellationToken)
{
KeyVaultCalls++;
if (KeyVaultException is not null) throw KeyVaultException;
return Task.FromResult(KeyVaultResult);
}
protected override Task<bool> SendStartupEmailAsync(CancellationToken cancellationToken)
{
MailerCalls++;
return Task.FromResult(MailerResult);
}
protected override Task<bool> ProbeDatabaseAsync(CancellationToken cancellationToken)
{
DatabaseCalls++;
return Task.FromResult(DatabaseResult);
}
protected override Task<bool> ProbeMfrAsync(CancellationToken cancellationToken)
{
MfrCalls++;
return Task.FromResult(MfrResult);
}
protected override Task<bool> ProbePdfLicenseAsync(CancellationToken cancellationToken)
{
PdfLicenseCalls++;
return Task.FromResult(PdfLicenseResult);
}
public Task RunForTestAsync(CancellationToken cancellationToken)
=> RunOnceAsync(cancellationToken);
}
/// <summary>Exposes the real (non-overridden) PDF license probe for direct testing.</summary>
private sealed class ProbeExposingStartupSelfTestService : StartupSelfTestService
{
public ProbeExposingStartupSelfTestService(
IServiceProvider serviceProvider,
IConfiguration configuration,
IOptions<StartupSelfTestSettings> settings,
Microsoft.Extensions.Logging.ILogger<StartupSelfTestService> logger)
: base(serviceProvider, configuration, settings, logger)
{
}
public Task<bool> InvokeProbePdfLicenseAsync(CancellationToken cancellationToken)
=> ProbePdfLicenseAsync(cancellationToken);
}
}