using NATS.Client.Core; using NATS.E2E.Tests.Infrastructure; namespace NATS.E2E.Tests; [Collection("E2E-Auth")] public class AuthTests(AuthServerFixture fixture) { [Fact] public async Task UsernamePassword_ValidCredentials_Connects() { await using var client = fixture.CreateClient("testuser", "testpass"); await client.ConnectAsync(); await client.PingAsync(); client.ConnectionState.ShouldBe(NatsConnectionState.Open); } [Fact] public async Task UsernamePassword_InvalidPassword_Rejected() { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Port}", AuthOpts = new NatsAuthOpts { Username = "testuser", Password = "wrongpass" }, MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ex.ShouldNotBeNull(); } [Fact] public async Task UsernamePassword_NoCredentials_Rejected() { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ex.ShouldNotBeNull(); } [Fact] public async Task TokenAuth_ValidToken_Connects() { var config = """ authorization { token: "s3cret" } """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{server.Port}", AuthOpts = new NatsAuthOpts { Token = "s3cret" }, }); await client.ConnectAsync(); await client.PingAsync(); client.ConnectionState.ShouldBe(NatsConnectionState.Open); } [Fact] public async Task TokenAuth_InvalidToken_Rejected() { var config = """ authorization { token: "s3cret" } """; await using var server = NatsServerProcess.WithConfig(config); await server.StartAsync(); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{server.Port}", AuthOpts = new NatsAuthOpts { Token = "wrongtoken" }, MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ex.ShouldNotBeNull(); } [Fact] public async Task NKeyAuth_ValidSignature_Connects() { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Port}", AuthOpts = new NatsAuthOpts { NKey = fixture.NKeyPublicKey, Seed = fixture.NKeySeed, }, }); await client.ConnectAsync(); await client.PingAsync(); client.ConnectionState.ShouldBe(NatsConnectionState.Open); } [Fact] public async Task NKeyAuth_InvalidSignature_Rejected() { // Generate a fresh key pair that is NOT registered with the server var otherSeed = NATS.Client.Core.NKeys.CreateUserSeed(); var otherPublicKey = NATS.Client.Core.NKeys.PublicKeyFromSeed(otherSeed); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Port}", AuthOpts = new NatsAuthOpts { NKey = otherPublicKey, Seed = otherSeed, }, MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await client.ConnectAsync(); await client.PingAsync(); }); ex.ShouldNotBeNull(); } [Fact] public async Task Permission_PublishAllowed_Succeeds() { await using var pub = fixture.CreateClient("pubonly", "pubpass"); await using var sub = fixture.CreateClient("testuser", "testpass"); await pub.ConnectAsync(); await sub.ConnectAsync(); await using var subscription = await sub.SubscribeCoreAsync("allowed.foo"); await sub.PingAsync(); await pub.PublishAsync("allowed.foo", "hello"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msg = await subscription.Msgs.ReadAsync(cts.Token); msg.Data.ShouldBe("hello"); } [Fact] public async Task Permission_PublishDenied_NoDelivery() { await using var pub = fixture.CreateClient("pubonly", "pubpass"); await using var sub = fixture.CreateClient("testuser", "testpass"); await pub.ConnectAsync(); await sub.ConnectAsync(); await using var subscription = await sub.SubscribeCoreAsync("denied.foo"); await sub.PingAsync(); await pub.PublishAsync("denied.foo", "should-not-arrive"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask(); await Should.ThrowAsync(async () => await readTask); } [Fact] public async Task Permission_SubscribeDenied_Rejected() { await using var pub = fixture.CreateClient("testuser", "testpass"); await using var sub = fixture.CreateClient("subonly", "subpass"); await pub.ConnectAsync(); await sub.ConnectAsync(); // subonly may not subscribe to denied.topic — the subscription silently // fails or is dropped by the server; no message should arrive await using var subscription = await sub.SubscribeCoreAsync("denied.topic"); await sub.PingAsync(); await pub.PublishAsync("denied.topic", "should-not-arrive"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask(); await Should.ThrowAsync(async () => await readTask); } [Fact] public async Task MaxSubscriptions_ExceedsLimit_Rejected() { // The server is configured with max_subs: 5 (server-wide). // Open a single connection and exhaust the limit. await using var client = fixture.CreateClient("limited", "limpass"); await client.ConnectAsync(); var subscriptions = new List(); try { // Create subscriptions up to the server max for (var i = 0; i < 5; i++) subscriptions.Add(await client.SubscribeCoreAsync($"max.subs.test.{i}")); // The 6th subscription should cause the server to close the connection var ex = await Should.ThrowAsync(async () => { subscriptions.Add(await client.SubscribeCoreAsync("max.subs.test.6")); // Force a round-trip so the server error is observed await client.PingAsync(); }); ex.ShouldNotBeNull(); } finally { foreach (var s in subscriptions) await s.DisposeAsync(); } } [Fact] public async Task MaxPayload_ExceedsLimit_Disconnected() { // The fixture server has max_payload: 512; send > 512 bytes await using var client = fixture.CreateClient("testuser", "testpass"); await client.ConnectAsync(); var oversized = new string('x', 600); var ex = await Should.ThrowAsync(async () => { await client.PublishAsync("payload.test", oversized); // Force a round-trip to observe the server's error response await client.PingAsync(); }); ex.ShouldNotBeNull(); } }