Verify mailer credentials against ProcessWeb API before showing E-Mail-Dienst as healthy
Playwright Tests / test (push) Canceled after 0s

The Admin status pill previously turned green whenever the mailer was
enabled in config, without proving the account/token actually work.
Add IComService.VerifyCredentialsAsync (GET /mailer/fc?fn=verify, also
passing serverid when configured so the API confirms this server
belongs to the account) and wire it into a new "email" system-status
probe, so the pill only goes green once the Mailer API itself confirms
the credentials.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-09-15 23:51:32 +02:00
co-authored by Claude Sonnet 5
parent f36f65789e
commit 107b7f54f1
10 changed files with 339 additions and 28 deletions
+133
View File
@@ -476,6 +476,139 @@ public class ProcessWebComServiceTests
Assert.Equal(0, handler.CallCount);
}
// ── Credential verification (GET /mailer/fc?fn=verify) ─────────────────────
private sealed class CapturingRequestHandler : HttpMessageHandler
{
private readonly HttpStatusCode _status;
private readonly string _body;
public HttpRequestMessage? LastRequest { get; private set; }
public CapturingRequestHandler(HttpStatusCode status, string body) { _status = status; _body = body; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
return Task.FromResult(new HttpResponseMessage(_status) { Content = new StringContent(_body) });
}
}
[Fact]
public async Task VerifyCredentialsAsync_MailerConfirms_ReturnsOkWithAccount()
{
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true,\"account\":\"acc-123\"}");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.True(result.Ok);
Assert.Equal("acc-123", result.Account);
Assert.NotNull(handler.LastRequest);
Assert.Equal(HttpMethod.Get, handler.LastRequest!.Method);
Assert.Equal("https://mailer.test/mailer/fc?fn=verify", handler.LastRequest.RequestUri!.ToString());
Assert.Equal("Basic", handler.LastRequest.Headers.Authorization!.Scheme);
}
[Fact]
public async Task VerifyCredentialsAsync_ServerIdConfigured_IsSentAndEchoedOnSuccess()
{
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true,\"account\":\"acc-123\",\"serverid\":\"srv-1\"}");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok", ServerId = "srv-1" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.True(result.Ok);
Assert.Equal("srv-1", result.ServerId);
Assert.Equal("https://mailer.test/mailer/fc?fn=verify&serverid=srv-1", handler.LastRequest!.RequestUri!.ToString());
}
[Fact]
public async Task VerifyCredentialsAsync_ServerIdNotOwnedByAccount_ReturnsNotOkWithServerIdDetail()
{
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":false,\"account\":\"acc-123\",\"serverid\":\"srv-wrong\"}");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok", ServerId = "srv-wrong" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.False(result.Ok);
Assert.Contains("srv-wrong", result.Detail);
}
[Fact]
public async Task VerifyCredentialsAsync_MailerReturnsOkFalse_ReturnsNotOk()
{
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":false}");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.False(result.Ok);
}
[Fact]
public async Task VerifyCredentialsAsync_Http401_ReturnsNotOkWithDetail()
{
var handler = new CapturingRequestHandler(HttpStatusCode.Unauthorized, "unauthorized");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "wrong" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.False(result.Ok);
Assert.Contains("401", result.Detail);
}
[Fact]
public async Task VerifyCredentialsAsync_TransportException_ReturnsNotOkWithExceptionDetail()
{
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(new ThrowingHandler()));
var result = await svc.VerifyCredentialsAsync();
Assert.False(result.Ok);
Assert.Contains("simulated transport failure", result.Detail);
}
[Theory]
[InlineData("", "tok")]
[InlineData("acct", "")]
public async Task VerifyCredentialsAsync_CredentialsNotConfigured_ReturnsNotOkWithoutCallingApi(string accountId, string token)
{
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true}");
var svc = new ProcessWebComService(
NullLogger<ProcessWebComService>.Instance, intranet: null!,
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = accountId, Token = token }),
Options.Create(new FuchsEmailSettings()),
new StubHttpClientFactory(handler));
var result = await svc.VerifyCredentialsAsync();
Assert.False(result.Ok);
Assert.Null(handler.LastRequest);
}
// ── Metric emission (performance indicator) ────────────────────────────────
[Fact]
public async Task SendEmailAsync_Success_IncrementsEmailsSentCounter()
+65 -1
View File
@@ -60,13 +60,20 @@ public class SystemStatusServiceTests
var env = new Mock<IHostEnvironment>();
env.SetupGet(e => e.EnvironmentName).Returns("Development");
var comMock = com ?? new Mock<IComService>();
if (com is null)
{
comMock.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = true, Account = "acc" });
}
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,
comMock.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 }),
@@ -277,6 +284,63 @@ public class SystemStatusServiceTests
Assert.False(r.Ok);
}
// ── Email probe ────────────────────────────────────────────────────────────
[Fact]
public async Task ProbeEmail_MailerDisabled_ReturnsDisabled()
{
var svc = Build(mailer: new ProcessWebComSettings { Enabled = false, AccountId = "acc", Token = "tok" });
var r = await svc.ProbeAsync("email");
Assert.Equal("email", r.Component);
Assert.Equal("disabled", r.Status);
Assert.False(r.Ok);
}
[Theory]
[InlineData("", "tok")]
[InlineData("acc", "")]
public async Task ProbeEmail_CredentialsMissing_ReturnsUnconfigured(string accountId, string token)
{
var svc = Build(mailer: new ProcessWebComSettings { Enabled = true, AccountId = accountId, Token = token });
var r = await svc.ProbeAsync("email");
Assert.Equal("unconfigured", r.Status);
Assert.False(r.Ok);
}
[Fact]
public async Task ProbeEmail_MailerConfirms_ReturnsOkWithAccount()
{
var com = new Mock<IComService>();
com.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = true, Account = "acc-123" });
var svc = Build(com: com);
var r = await svc.ProbeAsync("email");
Assert.Equal("email", r.Component);
Assert.Equal("ok", r.Status);
Assert.True(r.Ok);
Assert.Equal("acc-123", r.Detail);
}
[Fact]
public async Task ProbeEmail_MailerRejectsCredentials_ReturnsError()
{
var com = new Mock<IComService>();
com.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = false, Detail = "API-Fehler 401 (Unauthorized): {}" });
var svc = Build(com: com);
var r = await svc.ProbeAsync("email");
Assert.Equal("error", r.Status);
Assert.False(r.Ok);
Assert.Equal("API-Fehler 401 (Unauthorized): {}", r.Detail);
}
// ── Key Vault probe ────────────────────────────────────────────────────────
[Fact]
public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured()