- 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
263 lines
8.1 KiB
C#
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();
|
|
}
|
|
}
|