56 lines
2.4 KiB
C#
56 lines
2.4 KiB
C#
using System.Collections.Concurrent;
|
|
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ItWorx.KnowledgeHub.IntegrationTests.Operations;
|
|
|
|
/// <summary>Proves docs/19-observability.md's "Never log ... passwords" rule with a
|
|
/// real request through the real logging pipeline, not by code review: every log line
|
|
/// emitted while handling a real login attempt (wrong password, so the flow visits the
|
|
/// most log-heavy failure path) is captured and asserted to never contain the raw
|
|
/// password value.</summary>
|
|
[Collection(PostgresTestGroup.Name)]
|
|
public sealed class LoginRedactionTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
|
{
|
|
[Fact]
|
|
public async Task No_log_line_emitted_during_a_login_attempt_contains_the_raw_password()
|
|
{
|
|
const string secretPassword = "u3q7VeryUniquePasswordNeverLogged9Zx";
|
|
var captured = new ConcurrentQueue<string>();
|
|
|
|
using var loggingFactory = Factory.WithWebHostBuilder(builder =>
|
|
builder.ConfigureLogging(logging => logging.AddProvider(new CapturingLoggerProvider(captured))));
|
|
using var client = loggingFactory.CreateClient();
|
|
|
|
// A wrong-password attempt exercises the most log-heavy path (Identity
|
|
// failure handling, audit event, rate limiter) without needing the real
|
|
// owner password.
|
|
await SetupHelper.LoginAsync(client, OwnerEmail, secretPassword);
|
|
|
|
Assert.DoesNotContain(captured, line => line.Contains(secretPassword, StringComparison.Ordinal));
|
|
}
|
|
|
|
private sealed class CapturingLoggerProvider(ConcurrentQueue<string> sink) : ILoggerProvider
|
|
{
|
|
public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, sink);
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
|
|
private sealed class CapturingLogger(string categoryName, ConcurrentQueue<string> sink) : ILogger
|
|
{
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
public void Log<TState>(
|
|
LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
|
{
|
|
sink.Enqueue($"[{categoryName}] {formatter(state, exception)} {exception}");
|
|
}
|
|
}
|
|
}
|
|
}
|