Move 25 gateway-related test files from NATS.Server.Tests into a dedicated NATS.Server.Gateways.Tests project. Update namespaces, replace private ReadUntilAsync with SocketTestHelper from TestUtilities, inline TestServerFactory usage, add InternalsVisibleTo, and register the project in the solution file. All 261 tests pass.
158 lines
5.6 KiB
C#
158 lines
5.6 KiB
C#
// Go: gateway.go — per-account subscription routing state on outbound gateway connections.
|
|
// Gap 11.3: Account-specific gateway routes.
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using NATS.Server.Gateways;
|
|
using Shouldly;
|
|
|
|
namespace NATS.Server.Gateways.Tests.Gateways;
|
|
|
|
/// <summary>
|
|
/// Unit tests for account-specific subscription tracking on GatewayConnection.
|
|
/// Each test constructs a GatewayConnection using a connected socket pair so the
|
|
/// constructor succeeds; the subscription tracking methods are pure in-memory operations
|
|
/// that do not require the network handshake to have completed.
|
|
/// Go reference: gateway.go — account-scoped subscription propagation on outbound routes.
|
|
/// </summary>
|
|
public class AccountGatewayRoutesTests : IAsyncDisposable
|
|
{
|
|
private readonly TcpListener _listener;
|
|
private readonly Socket _clientSocket;
|
|
private readonly Socket _serverSocket;
|
|
private readonly GatewayConnection _conn;
|
|
|
|
public AccountGatewayRoutesTests()
|
|
{
|
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
|
_listener.Start();
|
|
var port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
|
|
|
_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
_clientSocket.Connect(IPAddress.Loopback, port);
|
|
_serverSocket = _listener.AcceptSocket();
|
|
|
|
_conn = new GatewayConnection(_serverSocket);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _conn.DisposeAsync();
|
|
_clientSocket.Dispose();
|
|
_listener.Stop();
|
|
}
|
|
|
|
// Go: gateway.go — AddAccountSubscription records the subject under the account key.
|
|
[Fact]
|
|
public void AddAccountSubscription_adds_subject()
|
|
{
|
|
_conn.AddAccountSubscription("ACCT_A", "orders.created");
|
|
|
|
_conn.AccountSubscriptionCount("ACCT_A").ShouldBe(1);
|
|
}
|
|
|
|
// Go: gateway.go — GetAccountSubscriptions returns a snapshot of tracked subjects.
|
|
[Fact]
|
|
public void GetAccountSubscriptions_returns_subjects()
|
|
{
|
|
_conn.AddAccountSubscription("ACCT_B", "payments.processed");
|
|
_conn.AddAccountSubscription("ACCT_B", "payments.failed");
|
|
|
|
var subs = _conn.GetAccountSubscriptions("ACCT_B");
|
|
|
|
subs.ShouldContain("payments.processed");
|
|
subs.ShouldContain("payments.failed");
|
|
subs.Count.ShouldBe(2);
|
|
}
|
|
|
|
// Go: gateway.go — RemoveAccountSubscription removes a specific subject.
|
|
[Fact]
|
|
public void RemoveAccountSubscription_removes_subject()
|
|
{
|
|
_conn.AddAccountSubscription("ACCT_C", "foo.bar");
|
|
_conn.AddAccountSubscription("ACCT_C", "foo.baz");
|
|
|
|
_conn.RemoveAccountSubscription("ACCT_C", "foo.bar");
|
|
|
|
_conn.GetAccountSubscriptions("ACCT_C").ShouldNotContain("foo.bar");
|
|
_conn.GetAccountSubscriptions("ACCT_C").ShouldContain("foo.baz");
|
|
}
|
|
|
|
// Go: gateway.go — AccountSubscriptionCount reflects current tracked count.
|
|
[Fact]
|
|
public void AccountSubscriptionCount_tracks_count()
|
|
{
|
|
_conn.AddAccountSubscription("ACCT_D", "s1");
|
|
_conn.AddAccountSubscription("ACCT_D", "s2");
|
|
_conn.AddAccountSubscription("ACCT_D", "s3");
|
|
_conn.RemoveAccountSubscription("ACCT_D", "s2");
|
|
|
|
_conn.AccountSubscriptionCount("ACCT_D").ShouldBe(2);
|
|
}
|
|
|
|
// Go: gateway.go — each account maintains its own isolated subscription set.
|
|
[Fact]
|
|
public void Different_accounts_tracked_independently()
|
|
{
|
|
_conn.AddAccountSubscription("ACC_X", "shared.subject");
|
|
_conn.AddAccountSubscription("ACC_Y", "other.subject");
|
|
|
|
_conn.GetAccountSubscriptions("ACC_X").ShouldContain("shared.subject");
|
|
_conn.GetAccountSubscriptions("ACC_X").ShouldNotContain("other.subject");
|
|
|
|
_conn.GetAccountSubscriptions("ACC_Y").ShouldContain("other.subject");
|
|
_conn.GetAccountSubscriptions("ACC_Y").ShouldNotContain("shared.subject");
|
|
}
|
|
|
|
// Go: gateway.go — GetAccountSubscriptions returns empty set for a never-seen account.
|
|
[Fact]
|
|
public void GetAccountSubscriptions_returns_empty_for_unknown()
|
|
{
|
|
var subs = _conn.GetAccountSubscriptions("UNKNOWN_ACCOUNT");
|
|
|
|
subs.ShouldBeEmpty();
|
|
}
|
|
|
|
// Go: gateway.go — duplicate AddAccountSubscription calls are idempotent (set semantics).
|
|
[Fact]
|
|
public void AddAccountSubscription_deduplicates()
|
|
{
|
|
_conn.AddAccountSubscription("ACCT_E", "orders.>");
|
|
_conn.AddAccountSubscription("ACCT_E", "orders.>");
|
|
_conn.AddAccountSubscription("ACCT_E", "orders.>");
|
|
|
|
_conn.AccountSubscriptionCount("ACCT_E").ShouldBe(1);
|
|
}
|
|
|
|
// Go: gateway.go — removing a subject that was never added is a no-op (no exception).
|
|
[Fact]
|
|
public void RemoveAccountSubscription_no_error_for_unknown()
|
|
{
|
|
// Neither the account nor the subject has ever been added.
|
|
var act = () => _conn.RemoveAccountSubscription("NEVER_ADDED", "some.subject");
|
|
|
|
act.ShouldNotThrow();
|
|
}
|
|
|
|
// Go: gateway.go — an account can track many subjects simultaneously.
|
|
[Fact]
|
|
public void Multiple_subjects_per_account()
|
|
{
|
|
var subjects = new[] { "a.b", "c.d", "e.f.>", "g.*", "h" };
|
|
foreach (var s in subjects)
|
|
_conn.AddAccountSubscription("ACCT_F", s);
|
|
|
|
var result = _conn.GetAccountSubscriptions("ACCT_F");
|
|
|
|
result.Count.ShouldBe(subjects.Length);
|
|
foreach (var s in subjects)
|
|
result.ShouldContain(s);
|
|
}
|
|
|
|
// Go: gateway.go — AccountSubscriptionCount returns 0 for an account with no entries.
|
|
[Fact]
|
|
public void AccountSubscriptionCount_zero_for_unknown()
|
|
{
|
|
_conn.AccountSubscriptionCount("COMPLETELY_NEW_ACCOUNT").ShouldBe(0);
|
|
}
|
|
}
|