using System.Collections.Concurrent; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging; namespace ItWorx.KnowledgeHub.IntegrationTests.Operations; /// 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. [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(); 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 sink) : ILoggerProvider { public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, sink); public void Dispose() { } private sealed class CapturingLogger(string categoryName, ConcurrentQueue sink) : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { sink.Enqueue($"[{categoryName}] {formatter(state, exception)} {exception}"); } } } }