using NATS.Server.Auth; using NATS.Server.Protocol; namespace NATS.Server.Auth.Tests; public class ExternalAuthCalloutTests { [Fact] public void External_callout_authenticator_can_allow_and_deny_with_timeout_and_reason_mapping() { var authenticator = new ExternalAuthCalloutAuthenticator( new FakeExternalAuthClient(), TimeSpan.FromMilliseconds(50)); var allowed = authenticator.Authenticate(new ClientAuthContext { Opts = new ClientOptions { Username = "u", Password = "p" }, Nonce = [], }); allowed.ShouldNotBeNull(); allowed.Identity.ShouldBe("u"); var denied = authenticator.Authenticate(new ClientAuthContext { Opts = new ClientOptions { Username = "u", Password = "bad" }, Nonce = [], }); denied.ShouldBeNull(); var timeout = new ExternalAuthCalloutAuthenticator( new SlowExternalAuthClient(TimeSpan.FromMilliseconds(200)), TimeSpan.FromMilliseconds(30)); timeout.Authenticate(new ClientAuthContext { Opts = new ClientOptions { Username = "u", Password = "p" }, Nonce = [], }).ShouldBeNull(); } private sealed class FakeExternalAuthClient : IExternalAuthClient { public Task AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct) { if (request is { Username: "u", Password: "p" }) return Task.FromResult(new ExternalAuthDecision(true, "u", "A")); return Task.FromResult(new ExternalAuthDecision(false, Reason: "denied")); } } private sealed class SlowExternalAuthClient(TimeSpan delay) : IExternalAuthClient { public async Task AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var reg = ct.Register(() => tcs.TrySetCanceled(ct)); using var timer = new Timer(_ => tcs.TrySetResult(true), null, delay, Timeout.InfiniteTimeSpan); await tcs.Task; return new ExternalAuthDecision(true, "slow"); } } }