Adds per-account subscription tracking to GatewayConnection via AddAccountSubscription/RemoveAccountSubscription/GetAccountSubscriptions/ AccountSubscriptionCount, with corresponding SendAccountSubscriptions and GetAccountSubscriptions helpers on GatewayManager. Covered by 10 new unit tests.
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.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);
|
|
}
|
|
}
|