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