using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server; using NATS.Server.Auth; using NATS.Server.TestUtilities; namespace NATS.Server.Auth.Tests; public class AuthIntegrationTests { /// /// Checks whether any exception in the chain contains the given substring. /// The NATS client wraps server errors in outer NatsException messages, /// so the actual "Authorization Violation" may be in an inner exception. /// private static bool ExceptionChainContains(Exception ex, string substring) { Exception? current = ex; while (current != null) { if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase)) return true; current = current.InnerException; } return false; } private static (NatsServer server, int port, CancellationTokenSource cts) StartServer(NatsOptions options) { var port = TestPortAllocator.GetFreePort(); options.Port = port; var server = new NatsServer(options, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); return (server, port, cts); } private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options) { var (server, port, cts) = StartServer(options); await server.WaitForReadyAsync(); return (server, port, cts); } [Fact] public async Task Token_auth_success() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Authorization = "s3cr3t", }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://s3cr3t@127.0.0.1:{port}", }); await client.ConnectAsync(); await client.PingAsync(); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task Token_auth_failure_disconnects() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Authorization = "s3cr3t", }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://wrongtoken@127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' in exception chain, but got: {ex}"); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task UserPassword_auth_success() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Username = "admin", Password = "secret", }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://admin:secret@127.0.0.1:{port}", }); await client.ConnectAsync(); await client.PingAsync(); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task UserPassword_auth_failure_disconnects() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Username = "admin", Password = "secret", }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://admin:wrong@127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' in exception chain, but got: {ex}"); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task MultiUser_auth_success() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Users = [ new User { Username = "alice", Password = "pass1" }, new User { Username = "bob", Password = "pass2" }, ], }); try { await using var alice = new NatsConnection(new NatsOpts { Url = $"nats://alice:pass1@127.0.0.1:{port}", }); await using var bob = new NatsConnection(new NatsOpts { Url = $"nats://bob:pass2@127.0.0.1:{port}", }); await alice.ConnectAsync(); await alice.PingAsync(); await bob.ConnectAsync(); await bob.PingAsync(); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task No_credentials_when_auth_required_disconnects() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Authorization = "s3cr3t", }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' in exception chain, but got: {ex}"); } finally { await cts.CancelAsync(); server.Dispose(); } } [Fact] public async Task No_auth_configured_allows_all() { var (server, port, cts) = await StartServerAsync(new NatsOptions()); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await client.ConnectAsync(); await client.PingAsync(); } finally { await cts.CancelAsync(); server.Dispose(); } } }