From ad3b8905171e4e7e759eac25e5c02fe76c68a0fc Mon Sep 17 00:00:00 2001 From: ThorSP Date: Wed, 14 Oct 2026 06:14:00 +0300 Subject: [PATCH 1/2] test: a start logs no Data Protection warnings Co-Authored-By: Claude Opus 5.5 --- tests/CampusPulse.Api.Tests/StartupTests.cs | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/CampusPulse.Api.Tests/StartupTests.cs b/tests/CampusPulse.Api.Tests/StartupTests.cs index 9419c91..260f047 100644 --- a/tests/CampusPulse.Api.Tests/StartupTests.cs +++ b/tests/CampusPulse.Api.Tests/StartupTests.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace CampusPulse.Api.Tests; @@ -27,4 +30,31 @@ public void A_real_start_without_a_connection_string_fails_fast() Assert.Contains("ConnectionStrings__Postgres is not set", exception.ToString()); } + + private sealed class LoggingFactory(CollectingLoggerProvider logs) : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Testing"); + builder.UseSetting("ConnectionStrings:Postgres", + "Host=127.0.0.1;Port=1;Database=unused;Username=unused;Password=unused;Timeout=2"); + TestAuth.Configure(builder); + builder.ConfigureTestServices(services => services.AddSingleton(logs)); + } + } + + /// Data Protection comes with AddAuthentication but protects nothing here; it is + /// ephemeral on purpose and must not warn about unpersisted keys at every start (#39). + [Fact] + public async Task A_start_logs_no_Data_Protection_warnings() + { + var logs = new CollectingLoggerProvider(); + using var factory = new LoggingFactory(logs); + + // A request, so the app (and its hosted services) has fully started. + await factory.CreateClient().GetAsync("/health"); + + Assert.DoesNotContain(logs.Entries, e => + e.Category.StartsWith("Microsoft.AspNetCore.DataProtection", StringComparison.Ordinal) && e.Level >= LogLevel.Warning); + } } From 275b08f2cbd0da2738da71b4fe0083611c87ac8d Mon Sep 17 00:00:00 2001 From: ThorSP Date: Wed, 14 Oct 2026 06:15:45 +0300 Subject: [PATCH 2/2] fix: ephemeral Data Protection, no key warnings at startup AddAuthentication registers Data Protection, which nothing uses (bearer tokens only). With no home directory (the Pi's service users) its defaults pick an in-memory key repository and log three warnings at every start. Declare the ephemeral provider instead. The first test couldn't reproduce the warnings (a test process has a home directory, so the defaults pick a file repository): it now pins the provider; stage's log is the behavioural check. Co-Authored-By: Claude Opus 5.5 --- src/CampusPulse.Api/Program.cs | 6 ++++++ tests/CampusPulse.Api.Tests/StartupTests.cs | 23 +++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/CampusPulse.Api/Program.cs b/src/CampusPulse.Api/Program.cs index 5203e44..08bef24 100644 --- a/src/CampusPulse.Api/Program.cs +++ b/src/CampusPulse.Api/Program.cs @@ -7,6 +7,7 @@ using CampusPulse.Api.Push; using CampusPulse.Api.Users; using CampusPulse.Infrastructure; +using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); @@ -46,6 +47,11 @@ // Push notifications through Expo, and the background check of their receipts (see PushSetup). builder.Services.AddCampusPulsePush(); +// AddAuthentication registers Data Protection, which nothing here uses: bearer tokens only, no +// cookies or antiforgery. Ephemeral keys say so, and keep the startup log free of warnings about +// unpersisted keys. A feature that must read protected data after a restart persists keys first. +builder.Services.AddDataProtection().UseEphemeralDataProtectionProvider(); + // Microsoft sign-in (UPT accounts). Unconfigured, it fails closed: see AuthSetup. var auth = builder.Services.AddCampusPulseAuth(builder.Configuration); diff --git a/tests/CampusPulse.Api.Tests/StartupTests.cs b/tests/CampusPulse.Api.Tests/StartupTests.cs index 260f047..05c0472 100644 --- a/tests/CampusPulse.Api.Tests/StartupTests.cs +++ b/tests/CampusPulse.Api.Tests/StartupTests.cs @@ -1,8 +1,7 @@ +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace CampusPulse.Api.Tests; @@ -31,7 +30,7 @@ public void A_real_start_without_a_connection_string_fails_fast() Assert.Contains("ConnectionStrings__Postgres is not set", exception.ToString()); } - private sealed class LoggingFactory(CollectingLoggerProvider logs) : WebApplicationFactory + private sealed class SignInFactory : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { @@ -39,22 +38,20 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.UseSetting("ConnectionStrings:Postgres", "Host=127.0.0.1;Port=1;Database=unused;Username=unused;Password=unused;Timeout=2"); TestAuth.Configure(builder); - builder.ConfigureTestServices(services => services.AddSingleton(logs)); } } - /// Data Protection comes with AddAuthentication but protects nothing here; it is - /// ephemeral on purpose and must not warn about unpersisted keys at every start (#39). + /// Data Protection comes with AddAuthentication but protects nothing here (bearer tokens + /// only), so its keys are ephemeral on purpose. Left to its defaults, a service user with no home + /// directory (the Pi's) gets an in-memory key repository and three warnings at every start (#39). + /// The warnings themselves can't be reproduced here: a test process has a home directory, so the + /// defaults pick a file repository instead. This pins the configuration that avoids them. [Fact] - public async Task A_start_logs_no_Data_Protection_warnings() + public async Task Data_Protection_is_ephemeral_by_design() { - var logs = new CollectingLoggerProvider(); - using var factory = new LoggingFactory(logs); - - // A request, so the app (and its hosted services) has fully started. + using var factory = new SignInFactory(); await factory.CreateClient().GetAsync("/health"); - Assert.DoesNotContain(logs.Entries, e => - e.Category.StartsWith("Microsoft.AspNetCore.DataProtection", StringComparison.Ordinal) && e.Level >= LogLevel.Warning); + Assert.IsType(factory.Services.GetRequiredService()); } }