// Port of Go server/accounts_test.go — account routing, limits, and import/export parity tests. // Reference: golang/nats-server/server/accounts_test.go using NATS.Server.Auth; using NATS.Server.Imports; using ServerSubscriptions = NATS.Server.Subscriptions; namespace NATS.Server.Auth.Tests.Auth; /// /// Parity tests ported from Go server/accounts_test.go exercising account /// route mappings, connection limits, import/export cycle detection, /// system account, and JetStream resource limits. /// public class AccountGoParityTests { // ======================================================================== // TestAccountBasicRouteMapping // Go reference: accounts_test.go:TestAccountBasicRouteMapping // ======================================================================== [Fact] public void BasicRouteMapping_SubjectIsolation() { // Go: TestAccountBasicRouteMapping — messages are isolated to accounts. // Different accounts have independent subscription namespaces. using var accA = new Account("A"); using var accB = new Account("B"); // Add subscriptions to account A's SubList var subA = new ServerSubscriptions.Subscription { Subject = "foo", Sid = "1" }; accA.SubList.Insert(subA); // Account B should not see account A's subscriptions var resultB = accB.SubList.Match("foo"); resultB.PlainSubs.Length.ShouldBe(0); // Account A should see its own subscription var resultA = accA.SubList.Match("foo"); resultA.PlainSubs.Length.ShouldBe(1); resultA.PlainSubs[0].ShouldBe(subA); } // ======================================================================== // TestAccountWildcardRouteMapping // Go reference: accounts_test.go:TestAccountWildcardRouteMapping // ======================================================================== [Fact] public void WildcardRouteMapping_PerAccountMatching() { // Go: TestAccountWildcardRouteMapping — wildcards work per-account. using var acc = new Account("TEST"); var sub1 = new ServerSubscriptions.Subscription { Subject = "orders.*", Sid = "1" }; var sub2 = new ServerSubscriptions.Subscription { Subject = "orders.>", Sid = "2" }; acc.SubList.Insert(sub1); acc.SubList.Insert(sub2); var result = acc.SubList.Match("orders.new"); result.PlainSubs.Length.ShouldBe(2); var result2 = acc.SubList.Match("orders.new.item"); result2.PlainSubs.Length.ShouldBe(1); // only "orders.>" matches result2.PlainSubs[0].ShouldBe(sub2); } // ======================================================================== // Connection limits // Go reference: accounts_test.go:TestAccountConnsLimitExceededAfterUpdate // ======================================================================== [Fact] public void ConnectionLimit_ExceededAfterUpdate() { // Go: TestAccountConnsLimitExceededAfterUpdate — reducing max connections // below current count prevents new connections. using var acc = new Account("TEST") { MaxConnections = 5 }; // Add 5 clients for (ulong i = 1; i <= 5; i++) acc.AddClient(i).ShouldBeTrue(); acc.ClientCount.ShouldBe(5); // 6th client should fail acc.AddClient(6).ShouldBeFalse(); } [Fact] public void ConnectionLimit_RemoveAllowsNew() { // Go: removing a client frees a slot. using var acc = new Account("TEST") { MaxConnections = 2 }; acc.AddClient(1).ShouldBeTrue(); acc.AddClient(2).ShouldBeTrue(); acc.AddClient(3).ShouldBeFalse(); acc.RemoveClient(1); acc.AddClient(3).ShouldBeTrue(); } [Fact] public void ConnectionLimit_ZeroMeansUnlimited() { // Go: MaxConnections=0 means unlimited. using var acc = new Account("TEST") { MaxConnections = 0 }; for (ulong i = 1; i <= 100; i++) acc.AddClient(i).ShouldBeTrue(); acc.ClientCount.ShouldBe(100); } // ======================================================================== // Subscription limits // Go reference: accounts_test.go TestAccountUserSubPermsWithQueueGroups // ======================================================================== [Fact] public void SubscriptionLimit_Enforced() { // Go: TestAccountUserSubPermsWithQueueGroups — subscription count limits. using var acc = new Account("TEST") { MaxSubscriptions = 3 }; acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeFalse(); acc.SubscriptionCount.ShouldBe(3); } [Fact] public void SubscriptionLimit_DecrementAllowsNew() { using var acc = new Account("TEST") { MaxSubscriptions = 2 }; acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeFalse(); acc.DecrementSubscriptions(); acc.IncrementSubscriptions().ShouldBeTrue(); } // ======================================================================== // System account // Go reference: events_test.go:TestSystemAccountNewConnection // ======================================================================== [Fact] public void SystemAccount_IsSystemAccountFlag() { // Go: TestSystemAccountNewConnection — system account identification. using var sysAcc = new Account(Account.SystemAccountName) { IsSystemAccount = true }; using var globalAcc = new Account(Account.GlobalAccountName); sysAcc.IsSystemAccount.ShouldBeTrue(); sysAcc.Name.ShouldBe("$SYS"); globalAcc.IsSystemAccount.ShouldBeFalse(); globalAcc.Name.ShouldBe("$G"); } // ======================================================================== // Import/Export cycle detection // Go reference: accounts_test.go — addServiceImport with checkForImportCycle // ======================================================================== [Fact] public void ImportExport_DirectCycleDetected() { // Go: cycle detection prevents A importing from B when B imports from A. using var accA = new Account("A"); using var accB = new Account("B"); accA.AddServiceExport("svc.a", ServiceResponseType.Singleton, [accB]); accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]); // A imports from B accA.AddServiceImport(accB, "from.b", "svc.b"); // B importing from A would create a cycle: B -> A -> B var ex = Should.Throw(() => accB.AddServiceImport(accA, "from.a", "svc.a")); ex.Message.ShouldContain("cycle"); } [Fact] public void ImportExport_IndirectCycleDetected() { // Go: indirect cycles through A -> B -> C -> A are detected. using var accA = new Account("A"); using var accB = new Account("B"); using var accC = new Account("C"); accA.AddServiceExport("svc.a", ServiceResponseType.Singleton, [accC]); accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]); accC.AddServiceExport("svc.c", ServiceResponseType.Singleton, [accB]); // A -> B accA.AddServiceImport(accB, "from.b", "svc.b"); // B -> C accB.AddServiceImport(accC, "from.c", "svc.c"); // C -> A would close the cycle: C -> A -> B -> C var ex = Should.Throw(() => accC.AddServiceImport(accA, "from.a", "svc.a")); ex.Message.ShouldContain("cycle"); } [Fact] public void ImportExport_NoCycle_Succeeds() { // Go: linear import chain A -> B -> C is allowed. using var accA = new Account("A"); using var accB = new Account("B"); using var accC = new Account("C"); accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]); accC.AddServiceExport("svc.c", ServiceResponseType.Singleton, [accB]); accA.AddServiceImport(accB, "from.b", "svc.b"); accB.AddServiceImport(accC, "from.c", "svc.c"); // No exception — linear chain is allowed. } [Fact] public void ImportExport_UnauthorizedAccount_Throws() { // Go: unauthorized import throws. using var accA = new Account("A"); using var accB = new Account("B"); using var accC = new Account("C"); // B exports only to C, not A accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accC]); Should.Throw(() => accA.AddServiceImport(accB, "from.b", "svc.b")); } [Fact] public void ImportExport_NoExport_Throws() { // Go: importing a non-existent export throws. using var accA = new Account("A"); using var accB = new Account("B"); Should.Throw(() => accA.AddServiceImport(accB, "from.b", "svc.nonexistent")); } // ======================================================================== // Stream import/export // Go reference: accounts_test.go TestAccountBasicRouteMapping (stream exports) // ======================================================================== [Fact] public void StreamImportExport_BasicFlow() { // Go: basic stream export from A, imported by B. using var accA = new Account("A"); using var accB = new Account("B"); accA.AddStreamExport("events.>", [accB]); accB.AddStreamImport(accA, "events.>", "imported.events.>"); accB.Imports.Streams.Count.ShouldBe(1); accB.Imports.Streams[0].From.ShouldBe("events.>"); accB.Imports.Streams[0].To.ShouldBe("imported.events.>"); } [Fact] public void StreamImport_Unauthorized_Throws() { using var accA = new Account("A"); using var accB = new Account("B"); using var accC = new Account("C"); accA.AddStreamExport("events.>", [accC]); // only C authorized Should.Throw(() => accB.AddStreamImport(accA, "events.>", "imported.>")); } [Fact] public void StreamImport_NoExport_Throws() { using var accA = new Account("A"); using var accB = new Account("B"); Should.Throw(() => accB.AddStreamImport(accA, "nonexistent.>", "imported.>")); } // ======================================================================== // JetStream account limits // Go reference: accounts_test.go (JS limits section) // ======================================================================== [Fact] public void JetStreamLimits_MaxStreams_Enforced() { // Go: per-account JetStream stream limit. using var acc = new Account("TEST") { JetStreamLimits = new AccountLimits { MaxStreams = 2 }, }; acc.TryReserveStream().ShouldBeTrue(); acc.TryReserveStream().ShouldBeTrue(); acc.TryReserveStream().ShouldBeFalse(); acc.ReleaseStream(); acc.TryReserveStream().ShouldBeTrue(); } [Fact] public void JetStreamLimits_MaxConsumers_Enforced() { using var acc = new Account("TEST") { JetStreamLimits = new AccountLimits { MaxConsumers = 3 }, }; acc.TryReserveConsumer().ShouldBeTrue(); acc.TryReserveConsumer().ShouldBeTrue(); acc.TryReserveConsumer().ShouldBeTrue(); acc.TryReserveConsumer().ShouldBeFalse(); } [Fact] public void JetStreamLimits_MaxStorage_Enforced() { using var acc = new Account("TEST") { JetStreamLimits = new AccountLimits { MaxStorage = 1024 }, }; acc.TrackStorageDelta(512).ShouldBeTrue(); acc.TrackStorageDelta(512).ShouldBeTrue(); acc.TrackStorageDelta(1).ShouldBeFalse(); // would exceed acc.TrackStorageDelta(-256).ShouldBeTrue(); // free some acc.TrackStorageDelta(256).ShouldBeTrue(); } [Fact] public void JetStreamLimits_Unlimited_AllowsAny() { using var acc = new Account("TEST") { JetStreamLimits = AccountLimits.Unlimited, }; for (int i = 0; i < 100; i++) { acc.TryReserveStream().ShouldBeTrue(); acc.TryReserveConsumer().ShouldBeTrue(); } acc.TrackStorageDelta(long.MaxValue / 2).ShouldBeTrue(); } // ======================================================================== // Account stats tracking // Go reference: accounts_test.go TestAccountReqMonitoring // ======================================================================== [Fact] public void AccountStats_InboundOutbound() { // Go: TestAccountReqMonitoring — per-account message/byte stats. using var acc = new Account("TEST"); acc.IncrementInbound(10, 1024); acc.IncrementOutbound(5, 512); acc.InMsgs.ShouldBe(10); acc.InBytes.ShouldBe(1024); acc.OutMsgs.ShouldBe(5); acc.OutBytes.ShouldBe(512); } [Fact] public void AccountStats_CumulativeAcrossIncrements() { using var acc = new Account("TEST"); acc.IncrementInbound(10, 1024); acc.IncrementInbound(5, 512); acc.InMsgs.ShouldBe(15); acc.InBytes.ShouldBe(1536); } // ======================================================================== // User revocation // Go reference: accounts_test.go TestAccountClaimsUpdatesWithServiceImports // ======================================================================== [Fact] public void UserRevocation_RevokedBeforeIssuedAt() { // Go: TestAccountClaimsUpdatesWithServiceImports — user revocation by NKey. using var acc = new Account("TEST"); acc.RevokeUser("UABC123", 1000); // JWT issued at 999 (before revocation) is revoked acc.IsUserRevoked("UABC123", 999).ShouldBeTrue(); // JWT issued at 1000 (exactly at revocation) is revoked acc.IsUserRevoked("UABC123", 1000).ShouldBeTrue(); // JWT issued at 1001 (after revocation) is NOT revoked acc.IsUserRevoked("UABC123", 1001).ShouldBeFalse(); } [Fact] public void UserRevocation_WildcardRevokesAll() { using var acc = new Account("TEST"); acc.RevokeUser("*", 500); acc.IsUserRevoked("ANY_USER_1", 499).ShouldBeTrue(); acc.IsUserRevoked("ANY_USER_2", 500).ShouldBeTrue(); acc.IsUserRevoked("ANY_USER_3", 501).ShouldBeFalse(); } [Fact] public void UserRevocation_UnrevokedUser_NotRevoked() { using var acc = new Account("TEST"); acc.IsUserRevoked("UNKNOWN_USER", 1000).ShouldBeFalse(); } // ======================================================================== // Remove service/stream imports // Go reference: accounts_test.go TestAccountRouteMappingChangesAfterClientStart // ======================================================================== [Fact] public void RemoveServiceImport_RemovesCorrectly() { // Go: TestAccountRouteMappingChangesAfterClientStart — dynamic import removal. using var accA = new Account("A"); using var accB = new Account("B"); accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]); accA.AddServiceImport(accB, "from.b", "svc.b"); accA.Imports.Services.ContainsKey("from.b").ShouldBeTrue(); accA.RemoveServiceImport("from.b").ShouldBeTrue(); accA.Imports.Services.ContainsKey("from.b").ShouldBeFalse(); } [Fact] public void RemoveStreamImport_RemovesCorrectly() { using var accA = new Account("A"); using var accB = new Account("B"); accA.AddStreamExport("events.>", [accB]); accB.AddStreamImport(accA, "events.>", "imported.>"); accB.Imports.Streams.Count.ShouldBe(1); accB.RemoveStreamImport("events.>").ShouldBeTrue(); accB.Imports.Streams.Count.ShouldBe(0); } [Fact] public void RemoveNonexistent_ReturnsFalse() { using var acc = new Account("TEST"); acc.RemoveServiceImport("nonexistent").ShouldBeFalse(); acc.RemoveStreamImport("nonexistent").ShouldBeFalse(); } }