Add German localization and styling for administration module
- Introduced new JavaScript file `fis.admin_txt_de.js` for German translations of administration-related terms and messages. - Created `fis.admin.css` for styling the administration interface, including layout, cards, and buttons. - Added `fis.admin.de.js` for the main functionality of the administration module, implementing features such as system status checks and email testing. - Minified version of the German JavaScript file created as `fis.admin.de.min.js`. - Minified CSS file created as `fis.admin.min.css` for optimized loading.
This commit is contained in:
@@ -205,6 +205,54 @@ public class StartupSelfTestServiceTests
|
||||
Assert.Equal(1, service.MailerCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunOnce_Disabled_PublishesNotRunReport()
|
||||
{
|
||||
var reporter = new StartupCheckReporter();
|
||||
using var service = new TestableStartupSelfTestService(
|
||||
new ServiceCollection().BuildServiceProvider(),
|
||||
CreateConfiguration(),
|
||||
Options.Create(CreateSettings(enabled: false)),
|
||||
NullLogger<StartupSelfTestService>.Instance,
|
||||
reporter);
|
||||
|
||||
await service.RunForTestAsync(CancellationToken.None);
|
||||
|
||||
Assert.NotNull(reporter.Latest);
|
||||
Assert.False(reporter.Latest!.Ran);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunOnce_Enabled_PublishesReportWithPerCheckResults()
|
||||
{
|
||||
var reporter = new StartupCheckReporter();
|
||||
using var service = new TestableStartupSelfTestService(
|
||||
new ServiceCollection().BuildServiceProvider(),
|
||||
CreateConfiguration(),
|
||||
Options.Create(CreateSettings(enabled: true, checkKeyVault: true, checkDatabase: true, checkMfr: false, sendStartupEmail: true, startupRecipient: "ops@example.test", checkPdfLicense: false)),
|
||||
NullLogger<StartupSelfTestService>.Instance,
|
||||
reporter)
|
||||
{
|
||||
KeyVaultResult = true,
|
||||
DatabaseResult = false,
|
||||
MailerResult = true,
|
||||
};
|
||||
|
||||
await service.RunForTestAsync(CancellationToken.None);
|
||||
|
||||
var report = reporter.Latest;
|
||||
Assert.NotNull(report);
|
||||
Assert.True(report!.Ran);
|
||||
Assert.NotNull(report.CompletedUtc);
|
||||
var kv = Assert.Single(report.Items, i => i.Name == "KeyVault");
|
||||
Assert.True(kv.Enabled && kv.Ok);
|
||||
var db = Assert.Single(report.Items, i => i.Name == "Database");
|
||||
Assert.True(db.Enabled);
|
||||
Assert.False(db.Ok); // intentionally-failing path
|
||||
var mfr = Assert.Single(report.Items, i => i.Name == "MFR");
|
||||
Assert.False(mfr.Enabled); // disabled check reported as not-enabled
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ProbeThrows_ServiceDoesNotThrowAndContinuesRemainingChecks()
|
||||
{
|
||||
@@ -250,8 +298,9 @@ public class StartupSelfTestServiceTests
|
||||
IServiceProvider serviceProvider,
|
||||
IConfiguration configuration,
|
||||
IOptions<StartupSelfTestSettings> settings,
|
||||
Microsoft.Extensions.Logging.ILogger<StartupSelfTestService> logger)
|
||||
: base(serviceProvider, configuration, settings, logger)
|
||||
Microsoft.Extensions.Logging.ILogger<StartupSelfTestService> logger,
|
||||
StartupCheckReporter? reporter = null)
|
||||
: base(serviceProvider, configuration, settings, logger, reporter)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Fuchs.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the Admin module's read-only diagnostics service. Covers the passive config
|
||||
/// snapshot (SQL server parsing, email override reflection, presence flags), each probe's
|
||||
/// disabled/unconfigured/ok/error outcomes where deterministically reachable without live
|
||||
/// infrastructure, the test-email pipeline (success/failure/validation + HTML-encoding +
|
||||
/// override reporting), and the emitted probe telemetry.
|
||||
/// </summary>
|
||||
public class SystemStatusServiceTests
|
||||
{
|
||||
private const string FuchsMeterName = "Fuchs.Intranet";
|
||||
|
||||
private static IConfiguration Config(Dictionary<string, string?>? overrides = null)
|
||||
{
|
||||
var dict = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:fuchs_fds_ConnectionString"] =
|
||||
"Data Source=SQLHOST,1433;Initial Catalog=site_fuchs_test;User ID=fuchs_app;password='secret';",
|
||||
["ConnectionStrings:AzureBlobStorage_ConnectionString"] = "DefaultEndpointsProtocol=https;AccountName=acct;AccountKey=key==;",
|
||||
["Fuchs:IsTestDeployment"] = "true",
|
||||
["Fds:MFR_host"] = "portal.mobilefieldreport.com",
|
||||
["Fds:MFR_UserName"] = "mfruser",
|
||||
["Fds:SyncEnabled"] = "true",
|
||||
["SecretManagement:VaultUri"] = "https://vault.example/",
|
||||
["SecretManagement:AppName"] = "fuchs",
|
||||
["SecretManagement:ManagedSecretKeys:0"] = "Fuchs--Mailer--Token",
|
||||
["SecretManagement:ManagedSecretKeys:1"] = "ConnectionStrings--fuchs-fds-password",
|
||||
};
|
||||
if (overrides != null)
|
||||
foreach (var kv in overrides) dict[kv.Key] = kv.Value;
|
||||
return new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
|
||||
}
|
||||
|
||||
private static SystemStatusService Build(
|
||||
IConfiguration? config = null,
|
||||
Mock<IBlobStorageService>? blob = null,
|
||||
Mock<IMfrClientFactory>? mfr = null,
|
||||
Mock<IComService>? com = null,
|
||||
ProcessWebComSettings? mailer = null,
|
||||
FuchsEmailSettings? email = null,
|
||||
AzureBlobStorageSettings? blobSettings = null,
|
||||
IServiceProvider? provider = null,
|
||||
StartupCheckReporter? startupChecks = null)
|
||||
{
|
||||
var env = new Mock<IHostEnvironment>();
|
||||
env.SetupGet(e => e.EnvironmentName).Returns("Development");
|
||||
|
||||
return new SystemStatusService(
|
||||
config ?? Config(),
|
||||
env.Object,
|
||||
provider ?? new ServiceCollection().BuildServiceProvider(),
|
||||
(blob ?? new Mock<IBlobStorageService>()).Object,
|
||||
(mfr ?? new Mock<IMfrClientFactory>()).Object,
|
||||
(com ?? new Mock<IComService>()).Object,
|
||||
Options.Create(mailer ?? new ProcessWebComSettings { Enabled = true, Token = "tok", BaseUrl = "https://api.example", AccountId = "acc", ServerId = "srv" }),
|
||||
Options.Create(email ?? new FuchsEmailSettings()),
|
||||
Options.Create(blobSettings ?? new AzureBlobStorageSettings { Enabled = true }),
|
||||
startupChecks ?? new StartupCheckReporter(),
|
||||
NullLogger<SystemStatusService>.Instance);
|
||||
}
|
||||
|
||||
// ── GetInfo (passive snapshot) ─────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void GetInfo_ParsesConnectionStringAndReflectsConfiguration()
|
||||
{
|
||||
var svc = Build();
|
||||
|
||||
SystemInfoSnapshot info = svc.GetInfo();
|
||||
|
||||
Assert.Equal("Development", info.Environment);
|
||||
Assert.True(info.IsTestDeployment);
|
||||
Assert.True(info.Database.Configured);
|
||||
Assert.Equal("SQLHOST,1433", info.Database.Server);
|
||||
Assert.Equal("site_fuchs_test", info.Database.Catalog);
|
||||
Assert.Equal("fuchs_app", info.Database.UserId); // login only, never the password
|
||||
Assert.True(info.Mfr.CredentialsConfigured);
|
||||
Assert.Equal("portal.mobilefieldreport.com", info.Mfr.Host);
|
||||
Assert.True(info.Mfr.SyncEnabled);
|
||||
Assert.Equal("fuchs", info.KeyVault.AppName);
|
||||
Assert.Equal(2, info.KeyVault.ManagedSecretCount);
|
||||
Assert.False(info.KeyVault.ClientRegistered); // empty provider
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_NeverLeaksDatabasePassword()
|
||||
{
|
||||
var svc = Build();
|
||||
|
||||
SystemInfoSnapshot info = svc.GetInfo();
|
||||
|
||||
Assert.DoesNotContain("secret", info.Database.UserId ?? "");
|
||||
Assert.DoesNotContain("secret", info.Database.Server ?? "");
|
||||
Assert.DoesNotContain("secret", info.Database.Catalog ?? "");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void GetInfo_EmailOverride_ReflectedOnlyWhenConfigured(bool overrideSet)
|
||||
{
|
||||
var email = new FuchsEmailSettings { OverrideRecipient = overrideSet ? "safety@example.test" : "" };
|
||||
var svc = Build(email: email);
|
||||
|
||||
SystemInfoSnapshot info = svc.GetInfo();
|
||||
|
||||
Assert.Equal(overrideSet, info.Email.OverrideActive);
|
||||
Assert.Equal(overrideSet ? "safety@example.test" : null, info.Email.OverrideRecipient);
|
||||
Assert.True(info.Email.MailerEnabled); // Build() default enables the mailer
|
||||
Assert.True(info.Email.TokenConfigured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_StartupChecks_NullWhenServiceHasNotRun()
|
||||
{
|
||||
var svc = Build(startupChecks: new StartupCheckReporter()); // nothing set yet
|
||||
|
||||
Assert.Null(svc.GetInfo().StartupChecks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_StartupChecks_ReflectsReporterContents()
|
||||
{
|
||||
var reporter = new StartupCheckReporter();
|
||||
reporter.Set(new StartupCheckReport
|
||||
{
|
||||
Ran = true,
|
||||
CompletedUtc = DateTimeOffset.UtcNow,
|
||||
MachineName = "BUILD-HOST",
|
||||
Items =
|
||||
[
|
||||
new StartupCheckItem { Name = "Database", Enabled = true, Ok = true },
|
||||
new StartupCheckItem { Name = "Mailer", Enabled = false, Ok = false },
|
||||
],
|
||||
});
|
||||
var svc = Build(startupChecks: reporter);
|
||||
|
||||
var report = svc.GetInfo().StartupChecks;
|
||||
|
||||
Assert.NotNull(report);
|
||||
Assert.True(report!.Ran);
|
||||
Assert.Equal("BUILD-HOST", report.MachineName);
|
||||
Assert.Equal(2, report.Items.Count);
|
||||
Assert.Contains(report.Items, i => i.Name == "Database" && i.Enabled && i.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_DatabaseUnconfigured_WhenConnectionStringMissing()
|
||||
{
|
||||
var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" });
|
||||
var svc = Build(config);
|
||||
|
||||
Assert.False(svc.GetInfo().Database.Configured);
|
||||
}
|
||||
|
||||
// ── Blob probe ─────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task ProbeBlob_Disabled_ReturnsDisabled()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = false });
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("blob", r.Component);
|
||||
Assert.Equal("disabled", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeBlob_EnabledButUnconfigured_ReturnsUnconfigured()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = false, Detail = "keine Verbindungszeichenfolge" });
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("unconfigured", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeBlob_Reachable_ReturnsOkWithAccountDetail()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = true, Reachable = true, AccountName = "acct", Detail = "StorageV2 / Standard_LRS" });
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("ok", r.Status);
|
||||
Assert.True(r.Ok);
|
||||
Assert.Contains("acct", r.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeBlob_Reachable_ReportsFileCountsPerContainerAsMetrics()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity
|
||||
{
|
||||
Enabled = true, Configured = true, Reachable = true, AccountName = "acct",
|
||||
Detail = "StorageV2 / Standard_LRS",
|
||||
Containers = new List<BlobContainerCount>
|
||||
{
|
||||
new() { Name = "dev-fuchs-invoices", Exists = true, FileCount = 42 },
|
||||
new() { Name = "dev-fuchs-reminders", Exists = false, FileCount = 0 },
|
||||
},
|
||||
});
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("ok", r.Status);
|
||||
Assert.NotNull(r.Metrics);
|
||||
Assert.Equal(2, r.Metrics!.Count);
|
||||
var inv = Assert.Single(r.Metrics, m => m.Label == "dev-fuchs-invoices");
|
||||
Assert.Equal("42 Dateien", inv.Value);
|
||||
var rem = Assert.Single(r.Metrics, m => m.Label == "dev-fuchs-reminders");
|
||||
Assert.Equal("nicht vorhanden", rem.Value);
|
||||
Assert.Contains("42 Dateien gesamt", r.Message); // total across containers
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeBlob_FileCountUnavailable_ReportsMetricWithoutFailing()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity
|
||||
{
|
||||
Enabled = true, Configured = true, Reachable = true, AccountName = "acct",
|
||||
Containers = new List<BlobContainerCount>
|
||||
{
|
||||
new() { Name = "dev-fuchs-invoices", Exists = true, FileCount = -1 },
|
||||
},
|
||||
});
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("ok", r.Status);
|
||||
var inv = Assert.Single(r.Metrics!);
|
||||
Assert.Equal("Anzahl nicht ermittelbar", inv.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeBlob_ConfiguredButUnreachable_ReturnsError()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = true, Reachable = false, AccountName = "acct", Detail = "403 Forbidden" });
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
var r = await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.Equal("error", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
// ── Key Vault probe ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured()
|
||||
{
|
||||
var svc = Build(); // empty service provider → no SecretClient
|
||||
|
||||
var r = await svc.ProbeAsync("keyvault");
|
||||
|
||||
Assert.Equal("keyvault", r.Component);
|
||||
Assert.Equal("unconfigured", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeKeyVault_NoManagedKeys_ReturnsUnconfigured()
|
||||
{
|
||||
var config = Config(new()
|
||||
{
|
||||
["SecretManagement:ManagedSecretKeys:0"] = null,
|
||||
["SecretManagement:ManagedSecretKeys:1"] = null,
|
||||
});
|
||||
var svc = Build(config);
|
||||
|
||||
var r = await svc.ProbeAsync("keyvault");
|
||||
|
||||
Assert.Equal("unconfigured", r.Status);
|
||||
}
|
||||
|
||||
// ── MFR probe ──────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task ProbeMfr_FactoryThrows_ReturnsError()
|
||||
{
|
||||
var mfr = new Mock<IMfrClientFactory>();
|
||||
mfr.Setup(f => f.Create()).Throws(new InvalidOperationException("no credentials"));
|
||||
var svc = Build(mfr: mfr);
|
||||
|
||||
var r = await svc.ProbeAsync("mfr");
|
||||
|
||||
Assert.Equal("mfr", r.Component);
|
||||
Assert.Equal("error", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
Assert.Equal("no credentials", r.Detail);
|
||||
}
|
||||
|
||||
// ── Database probe (unconfigured is deterministic without a live server) ────
|
||||
[Fact]
|
||||
public async Task ProbeDatabase_NoConnectionString_ReturnsUnconfigured()
|
||||
{
|
||||
var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" });
|
||||
var svc = Build(config);
|
||||
|
||||
var r = await svc.ProbeAsync("database");
|
||||
|
||||
Assert.Equal("database", r.Component);
|
||||
Assert.Equal("unconfigured", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
// ── Unknown component ──────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task ProbeAsync_UnknownComponent_ReturnsError()
|
||||
{
|
||||
var svc = Build();
|
||||
|
||||
var r = await svc.ProbeAsync("does-not-exist");
|
||||
|
||||
Assert.Equal("error", r.Status);
|
||||
Assert.False(r.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeAllAsync_ReturnsOneResultPerComponent()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = false });
|
||||
var mfr = new Mock<IMfrClientFactory>();
|
||||
mfr.Setup(f => f.Create()).Throws(new InvalidOperationException("x"));
|
||||
var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" });
|
||||
var svc = Build(config, blob: blob, mfr: mfr);
|
||||
|
||||
var results = await svc.ProbeAllAsync();
|
||||
|
||||
Assert.Equal(svc.ProbeComponents.Count, results.Count);
|
||||
foreach (var comp in svc.ProbeComponents)
|
||||
Assert.Contains(results, r => r.Component == comp);
|
||||
Assert.All(results, r => Assert.True(r.DurationMs >= 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeAsync_EmitsProbeTelemetry()
|
||||
{
|
||||
var blob = new Mock<IBlobStorageService>();
|
||||
blob.Setup(b => b.CheckConnectivityAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BlobConnectivity { Enabled = false });
|
||||
var svc = Build(blob: blob);
|
||||
|
||||
long delta = 0;
|
||||
using var listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (inst, l) =>
|
||||
{
|
||||
if (inst.Meter.Name == FuchsMeterName && inst.Name == "fuchs.systemstatus.probes")
|
||||
l.EnableMeasurementEvents(inst);
|
||||
}
|
||||
};
|
||||
listener.SetMeasurementEventCallback<long>((_, value, _, _) => Interlocked.Add(ref delta, value));
|
||||
listener.Start();
|
||||
|
||||
await svc.ProbeAsync("blob");
|
||||
|
||||
Assert.True(delta >= 1, "fuchs.systemstatus.probes counter should increment per probe.");
|
||||
}
|
||||
|
||||
// ── Test email ─────────────────────────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("", "subj", "body")]
|
||||
[InlineData("to@example.test", "", "body")]
|
||||
public async Task SendTestEmailAsync_MissingRequiredFields_DoesNotSend(string to, string subject, string body)
|
||||
{
|
||||
var com = new Mock<IComService>();
|
||||
var svc = Build(com: com);
|
||||
|
||||
var r = await svc.SendTestEmailAsync(to, subject, body);
|
||||
|
||||
Assert.False(r.Sent);
|
||||
com.Verify(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendTestEmailAsync_Success_SendsHtmlEncodedBodyToRecipient()
|
||||
{
|
||||
string? capturedHtml = null, capturedTo = null, capturedSubject = null;
|
||||
var com = new Mock<IComService>();
|
||||
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
|
||||
.Callback<string, string, string, string, string, Dictionary<string, byte[]>?>(
|
||||
(_, subj, html, to, _, _) => { capturedSubject = subj; capturedHtml = html; capturedTo = to; })
|
||||
.ReturnsAsync(true);
|
||||
var svc = Build(com: com);
|
||||
|
||||
var r = await svc.SendTestEmailAsync("dest@example.test", "Betreff", "Zeile1<script>\nZeile2");
|
||||
|
||||
Assert.True(r.Sent);
|
||||
Assert.Equal("dest@example.test", capturedTo);
|
||||
Assert.Equal("Betreff", capturedSubject);
|
||||
Assert.NotNull(capturedHtml);
|
||||
Assert.Contains("<script>", capturedHtml); // HTML-encoded, not injected
|
||||
Assert.DoesNotContain("<script>", capturedHtml);
|
||||
Assert.Contains("<br/>", capturedHtml); // newline → <br/>
|
||||
Assert.Null(r.OverrideRecipient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendTestEmailAsync_OverrideActive_ReportsOverrideRecipient()
|
||||
{
|
||||
var com = new Mock<IComService>();
|
||||
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
|
||||
.ReturnsAsync(true);
|
||||
var svc = Build(com: com, email: new FuchsEmailSettings { OverrideRecipient = "safety@example.test" });
|
||||
|
||||
var r = await svc.SendTestEmailAsync("dest@example.test", "Betreff", "Body");
|
||||
|
||||
Assert.True(r.Sent);
|
||||
Assert.Equal("dest@example.test", r.RequestedRecipient);
|
||||
Assert.Equal("safety@example.test", r.OverrideRecipient);
|
||||
Assert.Contains("safety@example.test", r.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendTestEmailAsync_MailerRejects_ReturnsNotSent()
|
||||
{
|
||||
var com = new Mock<IComService>();
|
||||
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
|
||||
.ReturnsAsync(false);
|
||||
var svc = Build(com: com);
|
||||
|
||||
var r = await svc.SendTestEmailAsync("dest@example.test", "Betreff", "Body");
|
||||
|
||||
Assert.False(r.Sent);
|
||||
Assert.Equal("dest@example.test", r.RequestedRecipient);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user