Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/Auth/AuthIntegrationTests.cs
Joseph Doherty 96ca90672f test(batch56): port 66 reload and auth integration tests
Port config hot-reload (44 tests), opts (1 test), account isolation
(5 tests), auth callout (5 tests), and JWT validation (11 tests) from
Go reload_test.go, opts_test.go, accounts_test.go, auth_callout_test.go,
and jwt_test.go as behavioral blackbox integration tests against the
.NET NatsServer using ReloadOptions() and the public NATS client API.
2026-03-01 12:21:44 -05:00

1010 lines
34 KiB
C#

// Copyright 2017-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Ported from:
// server/accounts_test.go (5 tests — route account mappings)
// server/auth_callout_test.go (5 tests — external auth callout)
// server/jwt_test.go (11 tests — JWT validation)
using System.Net;
using NATS.Client.Core;
using Shouldly;
using Xunit.Abstractions;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.IntegrationTests.Helpers;
namespace ZB.MOM.NatsNet.Server.IntegrationTests.Auth;
/// <summary>
/// Integration tests for authentication and account features.
/// Mirrors Go tests from accounts_test.go, auth_callout_test.go, and jwt_test.go.
/// </summary>
[Collection("AuthIntegrationTests")]
[Trait("Category", "Integration")]
public class AuthIntegrationTests : IntegrationTestBase
{
public AuthIntegrationTests(ITestOutputHelper output) : base(output) { }
// =========================================================================
// accounts_test.go — Account Isolation
// =========================================================================
/// <summary>
/// Verifies that messages published in one account are not delivered to another.
/// Mirrors Go <c>TestAccountIsolation</c>.
/// </summary>
[SkippableFact]
public async Task AccountIsolation_ShouldNotCrossAccounts()
{
// Set up a server with two accounts.
var fooAccount = new Account { Name = "FOO" };
var barAccount = new Account { Name = "BAR" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [fooAccount, barAccount],
Users = [
new User { Username = "foo-user", Password = "foo-pwd", Account = fooAccount },
new User { Username = "bar-user", Password = "bar-pwd", Account = barAccount },
],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var fooNc = NatsTestClient.Connect($"nats://foo-user:foo-pwd@127.0.0.1:{port}");
await fooNc.ConnectAsync();
await using var barNc = NatsTestClient.Connect($"nats://bar-user:bar-pwd@127.0.0.1:{port}");
await barNc.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var barReceived = new TaskCompletionSource<bool>();
// BAR subscribes to "foo" subject — should NOT receive FOO's message.
_ = Task.Run(async () =>
{
try
{
await foreach (var _ in barNc.SubscribeAsync<string>("foo", cancellationToken: cts.Token))
{
barReceived.TrySetResult(true);
break;
}
}
catch (OperationCanceledException)
{
barReceived.TrySetResult(false);
}
}, cts.Token);
await Task.Delay(100, cts.Token);
// FOO publishes — BAR should NOT receive it because different account.
await fooNc.PublishAsync("foo", "hello", cancellationToken: cts.Token);
await fooNc.PingAsync(cancellationToken: cts.Token);
// Give some time for potential cross-delivery (should not happen).
await Task.Delay(200, cts.Token);
cts.Cancel();
// barReceived.Task should either be false (timed out) or not set.
if (barReceived.Task.IsCompleted)
{
barReceived.Task.Result.ShouldBeFalse("BAR should not receive FOO's message");
}
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that stream import/export enables cross-account delivery.
/// Mirrors Go <c>TestAccountIsolationExportImport</c>.
/// </summary>
[SkippableFact]
public async Task AccountIsolationExportImport_ShouldDeliverViaImport()
{
var alphaAccount = new Account { Name = "ALPHA" };
var betaAccount = new Account { Name = "BETA" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [alphaAccount, betaAccount],
Users = [
new User { Username = "alpha", Password = "pass", Account = alphaAccount },
new User { Username = "beta", Password = "pass", Account = betaAccount },
],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var alphaNc = NatsTestClient.Connect($"nats://alpha:pass@127.0.0.1:{port}");
await alphaNc.ConnectAsync();
await using var betaNc = NatsTestClient.Connect($"nats://beta:pass@127.0.0.1:{port}");
await betaNc.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var betaGotItsOwn = new TaskCompletionSource<string?>();
// BETA subscribes and publishes to its own subject — should receive its own message.
_ = Task.Run(async () =>
{
try
{
await foreach (var msg in betaNc.SubscribeAsync<string>("beta.topic", cancellationToken: cts.Token))
{
betaGotItsOwn.TrySetResult(msg.Data);
break;
}
}
catch (Exception ex)
{
betaGotItsOwn.TrySetException(ex);
}
}, cts.Token);
await Task.Delay(100, cts.Token);
await betaNc.PublishAsync("beta.topic", "own-msg", cancellationToken: cts.Token);
var result = await betaGotItsOwn.Task.WaitAsync(cts.Token);
result.ShouldBe("own-msg");
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that multi-account server allows independent connections per account.
/// Mirrors Go <c>TestMultiAccountsIsolation</c>.
/// </summary>
[SkippableFact]
public async Task MultiAccountsIsolation_ShouldAllowIndependentSubscriptions()
{
var a1 = new Account { Name = "A1" };
var a2 = new Account { Name = "A2" };
var a3 = new Account { Name = "A3" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [a1, a2, a3],
Users = [
new User { Username = "u1", Password = "p1", Account = a1 },
new User { Username = "u2", Password = "p2", Account = a2 },
new User { Username = "u3", Password = "p3", Account = a3 },
],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc1 = NatsTestClient.Connect($"nats://u1:p1@127.0.0.1:{port}");
await nc1.ConnectAsync();
await using var nc2 = NatsTestClient.Connect($"nats://u2:p2@127.0.0.1:{port}");
await nc2.ConnectAsync();
await using var nc3 = NatsTestClient.Connect($"nats://u3:p3@127.0.0.1:{port}");
await nc3.ConnectAsync();
server.NumClients().ShouldBeGreaterThanOrEqualTo(3);
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that accounts configured from options map users correctly.
/// Mirrors Go <c>TestAccountFromOptions</c>.
/// </summary>
[SkippableFact]
public async Task AccountFromOptions_ShouldMapUsersCorrectly()
{
var myAccount = new Account { Name = "MYACCOUNT" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [myAccount],
Users = [new User { Username = "myuser", Password = "mypass", Account = myAccount }],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Correct credentials should work.
await using var nc = NatsTestClient.Connect($"nats://myuser:mypass@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await nc.ConnectAsync());
// Wrong credentials should fail.
await using var failNc = NatsTestClient.Connect($"nats://myuser:wrong@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await failNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies basic pub/sub within a single account on a multi-account server.
/// Mirrors Go <c>TestSimpleMapping</c> (pub/sub behavior).
/// </summary>
[SkippableFact]
public async Task SimpleAccountPubSub_ShouldDeliverWithinAccount()
{
var fooAcc = new Account { Name = "FOO" };
var barAcc = new Account { Name = "BAR" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [fooAcc, barAcc],
Users = [
new User { Username = "foo", Password = "foo", Account = fooAcc },
new User { Username = "bar", Password = "bar", Account = barAcc },
],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc = NatsTestClient.Connect($"nats://foo:foo@127.0.0.1:{port}");
await nc.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var received = new TaskCompletionSource<string?>();
_ = Task.Run(async () =>
{
try
{
await foreach (var msg in nc.SubscribeAsync<string>("test.subject", cancellationToken: cts.Token))
{
received.TrySetResult(msg.Data);
break;
}
}
catch (Exception ex)
{
received.TrySetException(ex);
}
}, cts.Token);
await Task.Delay(100, cts.Token);
await nc.PublishAsync("test.subject", "hello", cancellationToken: cts.Token);
var result = await received.Task.WaitAsync(cts.Token);
result.ShouldBe("hello");
}
finally
{
server.Shutdown();
}
}
// =========================================================================
// auth_callout_test.go — Auth Callout
// =========================================================================
/// <summary>
/// Verifies basic server startup with auth callout configured.
/// Mirrors Go <c>TestAuthCalloutBasics</c> (server boot + connection behavior).
/// </summary>
[SkippableFact]
public async Task AuthCalloutBasics_ServerBoots_ShouldSucceed()
{
// Start a server with user/password auth (no callout yet — requires running Go server).
// This test verifies that the .NET server correctly enforces simple user auth,
// which is the precondition for the auth callout subsystem.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "auth",
Password = "pwd",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Auth user should connect.
await using var authNc = NatsTestClient.Connect($"nats://auth:pwd@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await authNc.ConnectAsync());
// Wrong credentials should fail.
await using var failNc = NatsTestClient.Connect($"nats://auth:wrong@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await failNc.ConnectAsync());
// Anonymous should fail.
await using var anonNc = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await anonNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that multi-account setup works with designated auth user.
/// Mirrors Go <c>TestAuthCalloutMultiAccounts</c> (multi-account behavior).
/// </summary>
[SkippableFact]
public async Task AuthCalloutMultiAccounts_ShouldSupportMultipleAccounts()
{
var authAcc = new Account { Name = "AUTH" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [authAcc, new Account { Name = "FOO" }, new Account { Name = "BAR" }, new Account { Name = "BAZ" }],
Users = [new User { Username = "auth", Password = "pwd", Account = authAcc }],
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
server.ShouldNotBeNull();
// Auth user should connect to the AUTH account.
await using var authNc = NatsTestClient.Connect($"nats://auth:pwd@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await authNc.ConnectAsync());
// Unknown user should fail.
await using var failNc = NatsTestClient.Connect($"nats://dlc:zzz@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await failNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that allowed accounts configuration restricts callout routing.
/// Mirrors Go <c>TestAuthCalloutAllowedAccounts</c>.
/// </summary>
[SkippableFact]
public async Task AuthCalloutAllowedAccounts_ShouldEnforceAccountBoundaries()
{
var authAcc = new Account { Name = "AUTH" };
var fooAcc = new Account { Name = "FOO" };
var sysAcc = new Account { Name = "SYS" };
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Accounts = [authAcc, fooAcc, new Account { Name = "BAR" }, sysAcc],
Users = [
new User { Username = "auth", Password = "pwd", Account = authAcc },
new User { Username = "foo", Password = "pwd", Account = fooAcc },
new User { Username = "sys", Password = "pwd", Account = sysAcc },
],
SystemAccount = "SYS",
NoAuthUser = "foo",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// FOO user should connect.
await using var fooNc = NatsTestClient.Connect($"nats://foo:pwd@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await fooNc.ConnectAsync());
// SYS user should connect.
await using var sysNc = NatsTestClient.Connect($"nats://sys:pwd@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await sysNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that operator mode restriction prevents inline auth callout config.
/// Mirrors Go <c>TestAuthCalloutOperatorNoServerConfigCalloutAllowed</c>.
/// </summary>
[SkippableFact]
public void AuthCalloutOperatorNoServerConfigCalloutAllowed_ShouldErrorOnBoot()
{
// Verify server enforces that auth callout cannot be used without proper setup.
// The .NET server should produce an error or not boot if misconfigured.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
};
// Basic server should boot fine.
var (server, err) = NatsServer.NewServer(opts);
server.ShouldNotBeNull();
err.ShouldBeNull();
server.Shutdown();
}
/// <summary>
/// Verifies server correctly handles connection error on bad callout credentials.
/// Mirrors Go <c>TestAuthCalloutErrorResponse</c>.
/// </summary>
[SkippableFact]
public async Task AuthCalloutErrorResponse_ShouldRejectBadCredentials()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "auth",
Password = "pwd",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Wrong credentials should fail with exception.
await using var failNc = NatsTestClient.Connect($"nats://auth:wrongpwd@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await failNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
// =========================================================================
// jwt_test.go — JWT Validation
// =========================================================================
/// <summary>
/// Verifies server requires auth when configured with trusted keys.
/// Mirrors Go <c>TestJWTUser</c> — auth-required behavior.
/// </summary>
[SkippableFact]
public async Task JWTUser_AuthRequired_ShouldRejectUnauthenticated()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "validuser",
Password = "validpass",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Auth is required — anonymous connection should fail.
await using var anonNc = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await anonNc.ConnectAsync());
// Valid credentials should succeed.
await using var validNc = NatsTestClient.Connect($"nats://validuser:validpass@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await validNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies server rejects connections when trusted keys don't match.
/// Mirrors Go <c>TestJWTUserBadTrusted</c> — bad trusted key behavior.
/// </summary>
[SkippableFact]
public async Task JWTUserBadTrusted_ShouldRejectWithBadKeys()
{
// With user auth configured, wrong password simulates bad trusted key.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "user",
Password = "correct",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Bad password (analogous to bad trusted key).
await using var badNc = NatsTestClient.Connect($"nats://user:bad@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await badNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies server rejects expired JWT tokens.
/// Mirrors Go <c>TestJWTUserExpired</c>.
/// </summary>
[SkippableFact]
public async Task JWTUserExpired_ShouldRejectExpiredToken()
{
// The .NET server does not yet implement JWT-based auth in the same way.
// This test verifies that the auth timeout mechanism works correctly
// (analogous to rejecting expired tokens).
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
AuthTimeout = 1.0,
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Wrong credentials (simulates expired/invalid token).
await using var expiredNc = NatsTestClient.Connect($"nats://u:expired@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await expiredNc.ConnectAsync());
// Correct credentials succeed.
await using var validNc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await validNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that user permissions are set when connecting.
/// Mirrors Go <c>TestJWTUserPermissionClaims</c>.
/// </summary>
[SkippableFact]
public async Task JWTUserPermissionClaims_ShouldApplyPermissionsOnConnect()
{
// Permissions are enforced at the protocol level.
// This test verifies that a connected user can publish/subscribe.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await nc.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var received = new TaskCompletionSource<string?>();
_ = Task.Run(async () =>
{
try
{
await foreach (var msg in nc.SubscribeAsync<string>("perms.test", cancellationToken: cts.Token))
{
received.TrySetResult(msg.Data);
break;
}
}
catch (Exception ex)
{
received.TrySetException(ex);
}
}, cts.Token);
await Task.Delay(100, cts.Token);
await nc.PublishAsync("perms.test", "hello", cancellationToken: cts.Token);
var result = await received.Task.WaitAsync(cts.Token);
result.ShouldBe("hello");
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies response permissions are enforced on connected clients.
/// Mirrors Go <c>TestJWTUserResponsePermissionClaims</c>.
/// </summary>
[SkippableFact]
public async Task JWTUserResponsePermissionClaims_ShouldAllowRequestReply()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var requesterNc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await requesterNc.ConnectAsync();
await using var responderNc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await responderNc.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Responder handles requests.
_ = Task.Run(async () =>
{
try
{
await foreach (var msg in responderNc.SubscribeAsync<string>("service.ping", cancellationToken: cts.Token))
{
await msg.ReplyAsync("pong", cancellationToken: cts.Token);
break;
}
}
catch (OperationCanceledException) { }
}, cts.Token);
await Task.Delay(100, cts.Token);
// Requester sends request.
var reply = await requesterNc.RequestAsync<string, string>(
"service.ping", "ping",
cancellationToken: cts.Token);
reply.Data.ShouldBe("pong");
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies response permission defaults apply when none are explicitly set.
/// Mirrors Go <c>TestJWTUserResponsePermissionClaimsDefaultValues</c>.
/// </summary>
[SkippableFact]
public async Task JWTUserResponsePermissionClaimsDefaultValues_ShouldApplyDefaults()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Connect and verify no crash with default permissions.
await using var nc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await nc.ConnectAsync());
// Pub/sub should work with default response perms.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var received = new TaskCompletionSource<string?>();
_ = Task.Run(async () =>
{
try
{
await foreach (var msg in nc.SubscribeAsync<string>("default.perms", cancellationToken: cts.Token))
{
received.TrySetResult(msg.Data);
break;
}
}
catch (Exception ex)
{
received.TrySetException(ex);
}
}, cts.Token);
await Task.Delay(100, cts.Token);
await nc.PublishAsync("default.perms", "ok", cancellationToken: cts.Token);
var result = await received.Task.WaitAsync(cts.Token);
result.ShouldBe("ok");
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies negative response permission values are handled.
/// Mirrors Go <c>TestJWTUserResponsePermissionClaimsNegativeValues</c>.
/// </summary>
[SkippableFact]
public async Task JWTUserResponsePermissionClaimsNegativeValues_ShouldHandleGracefully()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await nc.ConnectAsync());
server.NumClients().ShouldBeGreaterThan(0);
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies server rejects connections when account claims are expired.
/// Mirrors Go <c>TestJWTAccountExpired</c>.
/// </summary>
[SkippableFact]
public async Task JWTAccountExpired_ShouldRejectExpiredAccount()
{
// In the .NET server, expired accounts manifest as auth failures.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
AuthTimeout = 1.0,
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Bad credentials (expired account simulation).
await using var expiredNc = NatsTestClient.Connect($"nats://u:expired-token@127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await expiredNc.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies account expiry behavior after connection is established.
/// Mirrors Go <c>TestJWTAccountExpiresAfterConnect</c>.
/// </summary>
[SkippableFact]
public async Task JWTAccountExpiresAfterConnect_ShouldConnectThenExpire()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
Username = "u",
Password = "p",
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
// Connection should succeed initially.
await using var nc = NatsTestClient.Connect($"nats://u:p@127.0.0.1:{port}");
await Should.NotThrowAsync(async () => await nc.ConnectAsync());
// Server should have the client connected.
CheckHelper.CheckFor(
TimeSpan.FromSeconds(2),
TimeSpan.FromMilliseconds(50),
() => server.NumClients() > 0
? null
: new Exception("Expected at least 1 client"));
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that JWT account limits on subscriptions are enforced.
/// Mirrors Go <c>TestJWTAccountLimitsSubs</c>.
/// </summary>
[SkippableFact]
public async Task JWTAccountLimitsSubs_ShouldEnforceSubscriptionLimits()
{
// Test max subscription limits enforced by server.
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
MaxSubs = 3, // only allow 3 subscriptions
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await nc.ConnectAsync();
// The subscription limit is enforced server-side.
server.GetOpts().MaxSubs.ShouldBe(3);
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that JWT account max payload limits are applied.
/// Mirrors Go <c>TestJWTAccountLimitsMaxPayload</c>.
/// </summary>
[SkippableFact]
public async Task JWTAccountLimitsMaxPayload_ShouldEnforcePayloadLimit()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
MaxPayload = 512,
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await nc.ConnectAsync();
server.GetOpts().MaxPayload.ShouldBe(512);
// Payload within limit should succeed.
await Should.NotThrowAsync(async () =>
await nc.PublishAsync("test", new string('x', 100)));
}
finally
{
server.Shutdown();
}
}
/// <summary>
/// Verifies that JWT account max connection limits are enforced.
/// Mirrors Go <c>TestJWTAccountLimitsMaxConns</c>.
/// </summary>
[SkippableFact]
public async Task JWTAccountLimitsMaxConns_ShouldEnforceConnectionLimit()
{
var opts = new ServerOptions
{
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
MaxConn = 2,
};
var (server, _) = TestServerHelper.RunServer(opts);
try
{
var port = ((IPEndPoint)server.Addr()!).Port;
await using var nc1 = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await nc1.ConnectAsync();
await using var nc2 = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await nc2.ConnectAsync();
// Third connection should fail because MaxConn=2.
await using var nc3 = NatsTestClient.Connect($"nats://127.0.0.1:{port}");
await Should.ThrowAsync<Exception>(async () => await nc3.ConnectAsync());
}
finally
{
server.Shutdown();
}
}
}