Files
natsdotnet/tests/NATS.E2E.Tests/AuthTests.cs
Joseph Doherty c30e67a69d Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
2026-03-12 14:09:23 -04:00

263 lines
8.1 KiB
C#

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<Exception>(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<Exception>(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<Exception>(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<Exception>(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<string>("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<string>("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<OperationCanceledException>(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<string>("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<OperationCanceledException>(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<IAsyncDisposable>();
try
{
// Create subscriptions up to the server max
for (var i = 0; i < 5; i++)
subscriptions.Add(await client.SubscribeCoreAsync<string>($"max.subs.test.{i}"));
// The 6th subscription should cause the server to close the connection
var ex = await Should.ThrowAsync<Exception>(async () =>
{
subscriptions.Add(await client.SubscribeCoreAsync<string>("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<Exception>(async () =>
{
await client.PublishAsync("payload.test", oversized);
// Force a round-trip to observe the server's error response
await client.PingAsync();
});
ex.ShouldNotBeNull();
}
}