feat: Wave 6 batch 2 — accounts/auth, gateways, routes, JetStream API, JetStream cluster tests
Add comprehensive Go-parity test coverage across 5 subsystems: - Accounts/Auth: isolation, import/export, auth mechanisms, permissions (82 tests) - Gateways: connection, forwarding, interest mode, config (106 tests) - Routes: connection, subscription, forwarding, config validation (78 tests) - JetStream API: stream/consumer CRUD, pub/sub, features, admin (234 tests) - JetStream Cluster: streams, consumers, failover, meta (108 tests) Total: ~608 new test annotations across 22 files (+13,844 lines) All tests pass individually; suite total: 2,283 passing, 3 skipped
This commit is contained in:
599
tests/NATS.Server.Tests/Accounts/AuthMechanismTests.cs
Normal file
599
tests/NATS.Server.Tests/Accounts/AuthMechanismTests.cs
Normal file
@@ -0,0 +1,599 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests.Accounts;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for authentication mechanisms: username/password, token, NKey-based auth,
|
||||
/// no-auth-user fallback, multi-user, and AuthService orchestration.
|
||||
/// Reference: Go auth_test.go — TestUserClone*, TestNoAuthUser, TestUserConnectionDeadline, etc.
|
||||
/// Reference: Go accounts_test.go — TestAccountMapsUsers.
|
||||
/// </summary>
|
||||
public class AuthMechanismTests
|
||||
{
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
|
||||
{
|
||||
var port = GetFreePort();
|
||||
options.Port = port;
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
return (server, port, cts);
|
||||
}
|
||||
|
||||
private static bool ExceptionChainContains(Exception ex, string substring)
|
||||
{
|
||||
Exception? current = ex;
|
||||
while (current != null)
|
||||
{
|
||||
if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
current = current.InnerException;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go: TestUserCloneNilPermissions server/auth_test.go:34
|
||||
[Fact]
|
||||
public void User_with_nil_permissions()
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Username = "foo",
|
||||
Password = "bar",
|
||||
};
|
||||
|
||||
user.Permissions.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: TestUserClone server/auth_test.go:53
|
||||
[Fact]
|
||||
public void User_with_permissions_has_correct_fields()
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Username = "foo",
|
||||
Password = "bar",
|
||||
Permissions = new Permissions
|
||||
{
|
||||
Publish = new SubjectPermission { Allow = ["foo"] },
|
||||
Subscribe = new SubjectPermission { Allow = ["bar"] },
|
||||
},
|
||||
};
|
||||
|
||||
user.Username.ShouldBe("foo");
|
||||
user.Password.ShouldBe("bar");
|
||||
user.Permissions.ShouldNotBeNull();
|
||||
user.Permissions.Publish!.Allow![0].ShouldBe("foo");
|
||||
user.Permissions.Subscribe!.Allow![0].ShouldBe("bar");
|
||||
}
|
||||
|
||||
// Go: TestUserClonePermissionsNoLists server/auth_test.go:80
|
||||
[Fact]
|
||||
public void User_with_empty_permissions()
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Username = "foo",
|
||||
Password = "bar",
|
||||
Permissions = new Permissions(),
|
||||
};
|
||||
|
||||
user.Permissions!.Publish.ShouldBeNull();
|
||||
user.Permissions!.Subscribe.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: TestNoAuthUser (token auth success) server/auth_test.go:225
|
||||
[Fact]
|
||||
public async Task Token_auth_success()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Authorization = "s3cr3t",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://s3cr3t@127.0.0.1:{port}",
|
||||
});
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: auth mechanism — token auth failure
|
||||
[Fact]
|
||||
public async Task Token_auth_failure_disconnects()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Authorization = "s3cr3t",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://wrongtoken@127.0.0.1:{port}",
|
||||
MaxReconnectRetry = 0,
|
||||
});
|
||||
|
||||
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
});
|
||||
|
||||
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
||||
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: auth mechanism — user/password success
|
||||
[Fact]
|
||||
public async Task UserPassword_auth_success()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Username = "admin",
|
||||
Password = "secret",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://admin:secret@127.0.0.1:{port}",
|
||||
});
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: auth mechanism — user/password failure
|
||||
[Fact]
|
||||
public async Task UserPassword_auth_failure_disconnects()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Username = "admin",
|
||||
Password = "secret",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://admin:wrong@127.0.0.1:{port}",
|
||||
MaxReconnectRetry = 0,
|
||||
});
|
||||
|
||||
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
});
|
||||
|
||||
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
||||
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: TestNoAuthUser server/auth_test.go:225 — multi-user auth
|
||||
[Fact]
|
||||
public async Task MultiUser_auth_each_user_succeeds()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User { Username = "alice", Password = "pass1" },
|
||||
new User { Username = "bob", Password = "pass2" },
|
||||
],
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var alice = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://alice:pass1@127.0.0.1:{port}",
|
||||
});
|
||||
await using var bob = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://bob:pass2@127.0.0.1:{port}",
|
||||
});
|
||||
|
||||
await alice.ConnectAsync();
|
||||
await alice.PingAsync();
|
||||
await bob.ConnectAsync();
|
||||
await bob.PingAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: TestNoAuthUser server/auth_test.go:225 — wrong user password
|
||||
[Fact]
|
||||
public async Task MultiUser_wrong_password_fails()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User { Username = "alice", Password = "pass1" },
|
||||
],
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://alice:wrong@127.0.0.1:{port}",
|
||||
MaxReconnectRetry = 0,
|
||||
});
|
||||
|
||||
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
});
|
||||
|
||||
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
||||
$"Expected 'Authorization Violation', but got: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: auth mechanism — no credentials with auth required
|
||||
[Fact]
|
||||
public async Task No_credentials_when_auth_required_disconnects()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Authorization = "s3cr3t",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{port}",
|
||||
MaxReconnectRetry = 0,
|
||||
});
|
||||
|
||||
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
});
|
||||
|
||||
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
||||
$"Expected 'Authorization Violation', but got: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: auth mechanism — no auth configured allows all
|
||||
[Fact]
|
||||
public async Task No_auth_configured_allows_all()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions());
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{port}",
|
||||
});
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: TestNoAuthUser server/auth_test.go:225 — no_auth_user fallback
|
||||
[Fact]
|
||||
public async Task NoAuthUser_fallback_allows_unauthenticated_connection()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
|
||||
new User { Username = "bar", Password = "pwd2", Account = "BAR" },
|
||||
],
|
||||
NoAuthUser = "foo",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Connect without credentials — should use no_auth_user "foo"
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{port}",
|
||||
});
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
|
||||
// Explicit auth also still works
|
||||
await using var bar = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://bar:pwd2@127.0.0.1:{port}",
|
||||
});
|
||||
await bar.ConnectAsync();
|
||||
await bar.PingAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: TestNoAuthUser server/auth_test.go:225 — invalid pwd with no_auth_user still fails
|
||||
[Fact]
|
||||
public async Task NoAuthUser_wrong_password_still_fails()
|
||||
{
|
||||
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
|
||||
new User { Username = "bar", Password = "pwd2", Account = "BAR" },
|
||||
],
|
||||
NoAuthUser = "foo",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await using var client = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://bar:wrong@127.0.0.1:{port}",
|
||||
MaxReconnectRetry = 0,
|
||||
});
|
||||
|
||||
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
await client.PingAsync();
|
||||
});
|
||||
|
||||
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
||||
$"Expected auth violation, got: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Go: AuthService — tests the build logic for auth service
|
||||
[Fact]
|
||||
public void AuthService_build_with_no_auth_returns_not_required()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions());
|
||||
authService.IsAuthRequired.ShouldBeFalse();
|
||||
authService.NonceRequired.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Go: AuthService — tests the build logic for token auth
|
||||
[Fact]
|
||||
public void AuthService_build_with_token_marks_auth_required()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions { Authorization = "secret" });
|
||||
authService.IsAuthRequired.ShouldBeTrue();
|
||||
authService.NonceRequired.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Go: AuthService — tests the build logic for user/password auth
|
||||
[Fact]
|
||||
public void AuthService_build_with_user_password_marks_auth_required()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Username = "admin",
|
||||
Password = "secret",
|
||||
});
|
||||
authService.IsAuthRequired.ShouldBeTrue();
|
||||
authService.NonceRequired.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Go: AuthService — tests the build logic for nkey auth
|
||||
[Fact]
|
||||
public void AuthService_build_with_nkeys_marks_nonce_required()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
NKeys = [new NKeyUser { Nkey = "UABC123" }],
|
||||
});
|
||||
authService.IsAuthRequired.ShouldBeTrue();
|
||||
authService.NonceRequired.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: AuthService — tests the build logic for multi-user auth
|
||||
[Fact]
|
||||
public void AuthService_build_with_users_marks_auth_required()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "pass" }],
|
||||
});
|
||||
authService.IsAuthRequired.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: AuthService.Authenticate — token match
|
||||
[Fact]
|
||||
public void AuthService_authenticate_token_success()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
|
||||
var result = authService.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Token = "mytoken" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe("token");
|
||||
}
|
||||
|
||||
// Go: AuthService.Authenticate — token mismatch
|
||||
[Fact]
|
||||
public void AuthService_authenticate_token_failure()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
|
||||
var result = authService.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Token = "wrong" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: AuthService.Authenticate — user/password match
|
||||
[Fact]
|
||||
public void AuthService_authenticate_user_password_success()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "pass", Account = "acct-a" }],
|
||||
});
|
||||
|
||||
var result = authService.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "alice", Password = "pass" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe("alice");
|
||||
result.AccountName.ShouldBe("acct-a");
|
||||
}
|
||||
|
||||
// Go: AuthService.Authenticate — user/password mismatch
|
||||
[Fact]
|
||||
public void AuthService_authenticate_user_password_failure()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users = [new User { Username = "alice", Password = "pass" }],
|
||||
});
|
||||
|
||||
var result = authService.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "alice", Password = "wrong" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: AuthService.Authenticate — no auth user fallback
|
||||
[Fact]
|
||||
public void AuthService_authenticate_no_auth_user_fallback()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
Users =
|
||||
[
|
||||
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
|
||||
],
|
||||
NoAuthUser = "foo",
|
||||
});
|
||||
|
||||
// No credentials provided — should fall back to no_auth_user
|
||||
var result = authService.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions(),
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe("foo");
|
||||
result.AccountName.ShouldBe("FOO");
|
||||
}
|
||||
|
||||
// Go: AuthService.GenerateNonce — nonce generation
|
||||
[Fact]
|
||||
public void AuthService_generates_unique_nonces()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions
|
||||
{
|
||||
NKeys = [new NKeyUser { Nkey = "UABC" }],
|
||||
});
|
||||
|
||||
var nonce1 = authService.GenerateNonce();
|
||||
var nonce2 = authService.GenerateNonce();
|
||||
|
||||
nonce1.Length.ShouldBe(11);
|
||||
nonce2.Length.ShouldBe(11);
|
||||
// Extremely unlikely to be the same
|
||||
nonce1.ShouldNotBe(nonce2);
|
||||
}
|
||||
|
||||
// Go: AuthService.EncodeNonce — nonce encoding
|
||||
[Fact]
|
||||
public void AuthService_nonce_encoding_is_url_safe_base64()
|
||||
{
|
||||
var authService = AuthService.Build(new NatsOptions());
|
||||
var nonce = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5 };
|
||||
|
||||
var encoded = authService.EncodeNonce(nonce);
|
||||
|
||||
// Should not contain standard base64 padding or non-URL-safe characters
|
||||
encoded.ShouldNotContain("=");
|
||||
encoded.ShouldNotContain("+");
|
||||
encoded.ShouldNotContain("/");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user