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.
1010 lines
34 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|