Files
natsdotnet/tests/NATS.Server.Gateways.Tests/Gateways/GatewayInterestTrackerTests.cs
Joseph Doherty 9972b74bc3 refactor: extract NATS.Server.Gateways.Tests project
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.
2026-03-12 15:10:50 -04:00

242 lines
9.4 KiB
C#

// Go: gateway.go:100-150 (InterestMode enum), gateway.go:1500-1600 (switchToInterestOnlyMode)
using NATS.Server.Gateways;
namespace NATS.Server.Gateways.Tests.Gateways;
/// <summary>
/// Unit tests for GatewayInterestTracker — the per-connection interest mode state machine.
/// Covers Optimistic/InterestOnly modes, threshold-based switching, and per-account isolation.
/// Go reference: gateway_test.go, TestGatewaySwitchToInterestOnlyModeImmediately (line 6934),
/// TestGatewayAccountInterest (line 1794), TestGatewayAccountUnsub (line 1912).
/// </summary>
public class GatewayInterestTrackerTests
{
// Go: TestGatewayBasic server/gateway_test.go:399 — initial state is Optimistic
[Fact]
public void StartsInOptimisticMode()
{
var tracker = new GatewayInterestTracker();
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.Optimistic);
tracker.GetMode("ANY_ACCOUNT").ShouldBe(GatewayInterestMode.Optimistic);
}
// Go: TestGatewayBasic server/gateway_test.go:399 — optimistic mode forwards everything
[Fact]
public void OptimisticForwardsEverything()
{
var tracker = new GatewayInterestTracker();
tracker.ShouldForward("$G", "any.subject").ShouldBeTrue();
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
tracker.ShouldForward("$G", "deeply.nested.subject.path").ShouldBeTrue();
tracker.ShouldForward("ACCT", "foo").ShouldBeTrue();
}
// Go: TestGatewayAccountUnsub server/gateway_test.go:1912 — RS- adds to no-interest
[Fact]
public void TrackNoInterest_AddsToNoInterestSet()
{
var tracker = new GatewayInterestTracker();
tracker.TrackNoInterest("$G", "orders.created");
// Should not forward that specific subject in Optimistic mode
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
// Other subjects still forwarded
tracker.ShouldForward("$G", "orders.updated").ShouldBeTrue();
tracker.ShouldForward("$G", "payments.created").ShouldBeTrue();
}
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934 — threshold switch
[Fact]
public void SwitchesToInterestOnlyAfterThreshold()
{
const int threshold = 10;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
// Add subjects up to (but not reaching) the threshold
for (int i = 0; i < threshold - 1; i++)
tracker.TrackNoInterest("$G", $"subject.{i}");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
// One more crosses the threshold
tracker.TrackNoInterest("$G", $"subject.{threshold - 1}");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
}
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
[Fact]
public void InterestOnlyMode_OnlyForwardsTrackedSubjects()
{
const int threshold = 5;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
// Trigger mode switch
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("$G", $"noise.{i}");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
// Nothing forwarded until interest is explicitly tracked
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
// Track a positive interest
tracker.TrackInterest("$G", "orders.created");
// Now only that subject is forwarded
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
tracker.ShouldForward("$G", "orders.updated").ShouldBeFalse();
tracker.ShouldForward("$G", "payments.done").ShouldBeFalse();
}
// Go: TestGatewaySubjectInterest server/gateway_test.go:1972 — wildcard interest in InterestOnly
[Fact]
public void InterestOnlyMode_SupportsWildcards()
{
const int threshold = 3;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
// Trigger InterestOnly mode
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("$G", $"x.{i}");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
// Register a wildcard interest
tracker.TrackInterest("$G", "foo.>");
// Matching subjects are forwarded
tracker.ShouldForward("$G", "foo.bar").ShouldBeTrue();
tracker.ShouldForward("$G", "foo.bar.baz").ShouldBeTrue();
tracker.ShouldForward("$G", "foo.anything.deep.nested").ShouldBeTrue();
// Non-matching subjects are not forwarded
tracker.ShouldForward("$G", "other.subject").ShouldBeFalse();
tracker.ShouldForward("$G", "foo").ShouldBeFalse(); // "foo.>" requires at least one token after "foo"
}
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — per-account mode isolation
[Fact]
public void ModePerAccount()
{
const int threshold = 5;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
// Switch ACCT_A to InterestOnly
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("ACCT_A", $"noise.{i}");
tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.InterestOnly);
// ACCT_B remains Optimistic
tracker.GetMode("ACCT_B").ShouldBe(GatewayInterestMode.Optimistic);
// ACCT_A blocks unknown subjects, ACCT_B forwards
tracker.ShouldForward("ACCT_A", "orders.created").ShouldBeFalse();
tracker.ShouldForward("ACCT_B", "orders.created").ShouldBeTrue();
}
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
[Fact]
public void ModePersistsAfterSwitch()
{
const int threshold = 3;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
// Trigger switch
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("$G", $"y.{i}");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
// TrackInterest in InterestOnly mode — mode stays InterestOnly
tracker.TrackInterest("$G", "orders.created");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
// TrackNoInterest in InterestOnly mode — mode stays InterestOnly
tracker.TrackNoInterest("$G", "something.else");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
}
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — explicit SwitchToInterestOnly
[Fact]
public void ExplicitSwitchToInterestOnly_SetsMode()
{
var tracker = new GatewayInterestTracker();
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
tracker.SwitchToInterestOnly("$G");
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
}
// Go: TestGatewayAccountUnsub server/gateway_test.go:1912 — RS+ restores interest after RS-
[Fact]
public void TrackInterest_InOptimisticMode_RemovesFromNoInterestSet()
{
var tracker = new GatewayInterestTracker();
// Mark no interest
tracker.TrackNoInterest("$G", "orders.created");
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
// Remote re-subscribes — track interest again
tracker.TrackInterest("$G", "orders.created");
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
}
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
[Fact]
public void InterestOnlyMode_TrackNoInterest_RemovesFromInterestSet()
{
const int threshold = 3;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
// Trigger InterestOnly
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("$G", $"z.{i}");
tracker.TrackInterest("$G", "orders.created");
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
// Remote unsubscribes — subject removed from interest set
tracker.TrackNoInterest("$G", "orders.created");
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
}
// Go: TestGatewaySubjectInterest server/gateway_test.go:1972 — pwc wildcard in InterestOnly
[Fact]
public void InterestOnlyMode_SupportsPwcWildcard()
{
const int threshold = 3;
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
for (int i = 0; i < threshold; i++)
tracker.TrackNoInterest("$G", $"n.{i}");
tracker.TrackInterest("$G", "orders.*");
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
tracker.ShouldForward("$G", "orders.deleted").ShouldBeTrue();
tracker.ShouldForward("$G", "orders.deep.nested").ShouldBeFalse(); // * is single token
tracker.ShouldForward("$G", "payments.created").ShouldBeFalse();
}
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — unknown account defaults optimistic
[Fact]
public void UnknownAccount_DefaultsToOptimisticForwarding()
{
var tracker = new GatewayInterestTracker();
// Account never seen — should forward everything
tracker.ShouldForward("BRAND_NEW_ACCOUNT", "any.subject").ShouldBeTrue();
}
}