Logout tests: pin the loop's own tick-failure catch now that subject faults are isolated
A pass that cannot read its subjects (a scope factory failing like a database outage) is logged by exception type only and retried after the interval, never in a tight loop. The earlier loop test reached this catch through a per-subject exception, which the isolation now absorbs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8340ee0e92
commit
37c8fe1a72
2 changed files with 53 additions and 2 deletions
|
|
@ -439,6 +439,36 @@ public sealed class OidcLogoutTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_pass_that_cannot_read_its_subjects_is_logged_by_type_and_retried_after_the_interval()
|
||||
{
|
||||
using var factory = new OidcLogoutFactory();
|
||||
var clock = new ControlledClock();
|
||||
var log = new ListLogger<OidcLogoutReconciler>();
|
||||
var flaky = new FlakyScopeFactory(factory.Services.GetRequiredService<IServiceScopeFactory>(), failures: 1);
|
||||
var reconciler = factory.NewReconciler(clock, log, flaky);
|
||||
var (identity, _) = await factory.GrantAsync(MembershipStatus.Suspended);
|
||||
await reconciler.StartAsync(CancellationToken.None);
|
||||
try
|
||||
{
|
||||
await WaitForAsync(() => log.Entries.Any(x => x.Level == LogLevel.Error) && clock.Waiting > 0);
|
||||
var failure = Assert.Single(log.Entries, x => x.Level == LogLevel.Error);
|
||||
Assert.Contains(nameof(InvalidOperationException), failure.Message, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(FlakyScopeFactory.Secret, failure.Message, StringComparison.Ordinal);
|
||||
|
||||
// Never a tight loop: nothing posts until the interval passes, then the pass succeeds.
|
||||
await Task.Delay(300);
|
||||
Assert.Empty(factory.Mas.Bodies);
|
||||
clock.FireTimers();
|
||||
await WaitForAsync(() => factory.Mas.Bodies.Count == 1);
|
||||
await WaitForAsync(() => factory.ValidAuthorizations(factory.AccountId(identity).ToString("D")) == 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await reconciler.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> condition)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(10);
|
||||
|
|
|
|||
|
|
@ -317,6 +317,24 @@ public sealed class RedirectingMas : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>A scope factory that fails the first scopes asked of it, as a database outage would.</summary>
|
||||
public sealed class FlakyScopeFactory(IServiceScopeFactory inner, int failures) : IServiceScopeFactory
|
||||
{
|
||||
public const string Secret = "scope-factory-secret-detail";
|
||||
|
||||
private int _failures = failures;
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _failures) >= 0)
|
||||
{
|
||||
throw new InvalidOperationException(Secret);
|
||||
}
|
||||
|
||||
return inner.CreateScope();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>An OIDC host with MAS's back-channel logout URI set, a fake MAS behind it and a clock the test drives.</summary>
|
||||
public sealed class OidcLogoutFactory : OidcWebApplicationFactory
|
||||
{
|
||||
|
|
@ -361,8 +379,11 @@ public sealed class OidcLogoutFactory : OidcWebApplicationFactory
|
|||
}
|
||||
|
||||
/// <summary>A reconciler with no memory, built over this host's database, as a restarted process would have.</summary>
|
||||
public OidcLogoutReconciler NewReconciler(TimeProvider? time = null, ILogger<OidcLogoutReconciler>? logger = null) => new(
|
||||
Services.GetRequiredService<IServiceScopeFactory>(),
|
||||
public OidcLogoutReconciler NewReconciler(
|
||||
TimeProvider? time = null,
|
||||
ILogger<OidcLogoutReconciler>? logger = null,
|
||||
IServiceScopeFactory? scopes = null) => new(
|
||||
scopes ?? Services.GetRequiredService<IServiceScopeFactory>(),
|
||||
Services.GetRequiredService<OidcIssuerOptions>(),
|
||||
Services.GetRequiredService<OidcKeyMaterial>(),
|
||||
Services.GetRequiredService<IOptions<OpenIddictServerOptions>>(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue