// Copyright 2018-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/accounts_test.go and server/dirstore_test.go in the NATS server Go source. using Shouldly; using Xunit; namespace ZB.MOM.NatsNet.Server.Tests; [Collection("AccountTests")] public sealed partial class AccountTests { // ========================================================================= // Account Basic Tests // ========================================================================= // Test 1 [Fact] public void NewAccount_SetsNameAndUnlimitedLimits() { var acc = Account.NewAccount("foo"); acc.Name.ShouldBe("foo"); acc.MaxConnections.ShouldBe(-1); acc.MaxLeafNodes.ShouldBe(-1); } // Test 2 [Fact] public void ToString_ReturnsName() { var acc = Account.NewAccount("myaccount"); acc.ToString().ShouldBe(acc.Name); } // Test 3 [Fact] public void IsExpired_InitiallyFalse() { var acc = Account.NewAccount("foo"); acc.IsExpired().ShouldBeFalse(); } // Test 4 [Fact] public void IsClaimAccount_NoJwt_ReturnsFalse() { var acc = Account.NewAccount("foo"); // ClaimJwt defaults to empty string acc.IsClaimAccount().ShouldBeFalse(); } // Test 5 [Fact] public void NumConnections_Initial_IsZero() { var acc = Account.NewAccount("foo"); acc.NumConnections().ShouldBe(0); } // Test 6 [Fact] public void GetName_ReturnsName() { var acc = Account.NewAccount("thread-safe-name"); acc.GetName().ShouldBe("thread-safe-name"); } // ========================================================================= // Subject Mapping Tests // ========================================================================= // Test 7 [Fact] public void AddMapping_ValidSubject_Succeeds() { var acc = Account.NewAccount("foo"); var err = acc.AddMapping("foo", "bar"); err.ShouldBeNull(); } // Test 8 [Fact] public void AddMapping_InvalidSubject_ReturnsError() { var acc = Account.NewAccount("foo"); var err = acc.AddMapping("foo..bar", "x"); err.ShouldNotBeNull(); } // Test 9 [Fact] public void RemoveMapping_ExistingMapping_ReturnsTrue() { var acc = Account.NewAccount("foo"); acc.AddMapping("foo", "bar").ShouldBeNull(); var removed = acc.RemoveMapping("foo"); removed.ShouldBeTrue(); } // Test 10 [Fact] public void RemoveMapping_NonExistentMapping_ReturnsFalse() { var acc = Account.NewAccount("foo"); var removed = acc.RemoveMapping("nonexistent"); removed.ShouldBeFalse(); } // Test 11 [Fact] public void HasMappings_AfterAdd_ReturnsTrue() { var acc = Account.NewAccount("foo"); acc.AddMapping("foo", "bar").ShouldBeNull(); acc.HasMappings().ShouldBeTrue(); } // Test 12 [Fact] public void HasMappings_AfterRemove_ReturnsFalse() { var acc = Account.NewAccount("foo"); acc.AddMapping("foo", "bar").ShouldBeNull(); acc.RemoveMapping("foo"); acc.HasMappings().ShouldBeFalse(); } // Test 13 [Fact] public void SelectMappedSubject_NoMapping_ReturnsFalse() { var acc = Account.NewAccount("foo"); var (dest, mapped) = acc.SelectMappedSubject("foo"); mapped.ShouldBeFalse(); dest.ShouldBe("foo"); } // Test 14 [Fact] public void SelectMappedSubject_SimpleMapping_ReturnsMappedDest() { var acc = Account.NewAccount("foo"); acc.AddMapping("foo", "bar").ShouldBeNull(); var (dest, mapped) = acc.SelectMappedSubject("foo"); mapped.ShouldBeTrue(); dest.ShouldBe("bar"); } // Test 15 [Fact] public void AddWeightedMappings_DuplicateDest_ReturnsError() { var acc = Account.NewAccount("foo"); var err = acc.AddWeightedMappings("src", MapDest.New("dest1", 50), MapDest.New("dest1", 50)); // duplicate subject err.ShouldNotBeNull(); } // Test 16 [Fact] public void AddWeightedMappings_WeightOver100_ReturnsError() { var acc = Account.NewAccount("foo"); var err = acc.AddWeightedMappings("src", MapDest.New("dest1", 101)); // weight exceeds 100 err.ShouldNotBeNull(); } // Test 17 [Fact] public void AddWeightedMappings_TotalWeightOver100_ReturnsError() { var acc = Account.NewAccount("foo"); var err = acc.AddWeightedMappings("src", MapDest.New("dest1", 80), MapDest.New("dest2", 80)); // total = 160 err.ShouldNotBeNull(); } // ========================================================================= // Connection Counting Tests // ========================================================================= // Test 18 [Fact] public void NumLeafNodes_Initial_IsZero() { var acc = Account.NewAccount("foo"); acc.NumLeafNodes().ShouldBe(0); } // Test 19 [Fact] public void MaxTotalConnectionsReached_UnlimitedAccount_ReturnsFalse() { var acc = Account.NewAccount("foo"); // MaxConnections is -1 (unlimited) by default acc.MaxTotalConnectionsReached().ShouldBeFalse(); } // Test 20 [Fact] public void MaxTotalLeafNodesReached_UnlimitedAccount_ReturnsFalse() { var acc = Account.NewAccount("foo"); // MaxLeafNodes is -1 (unlimited) by default acc.MaxTotalLeafNodesReached().ShouldBeFalse(); } // ========================================================================= // Export Service Tests // ========================================================================= // Test 21 [Fact] public void IsExportService_NoExports_ReturnsFalse() { var acc = Account.NewAccount("foo"); acc.IsExportService("my.service").ShouldBeFalse(); } // Test 22 [Fact] public void IsExportServiceTracking_NoExports_ReturnsFalse() { var acc = Account.NewAccount("foo"); acc.IsExportServiceTracking("my.service").ShouldBeFalse(); } } // ========================================================================= // DirJwtStore Tests // ========================================================================= [Collection("AccountTests")] public sealed class DirJwtStoreTests : IDisposable { private readonly List _tempDirs = []; private string MakeTempDir() { var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(dir); _tempDirs.Add(dir); return dir; } public void Dispose() { foreach (var dir in _tempDirs) { try { Directory.Delete(dir, true); } catch { /* best effort */ } } } // Test 23 [Fact] public void DirJwtStore_WriteAndRead_Succeeds() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); const string key = "AAAAAAAAAA"; // minimum 2-char key const string jwt = "header.payload.signature"; store.SaveAcc(key, jwt); var loaded = store.LoadAcc(key); loaded.ShouldBe(jwt); } // Test 24 [Fact] public void DirJwtStore_ShardedWriteAndRead_Succeeds() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: true, create: false); var keys = new[] { "ACCTKEY001", "ACCTKEY002", "ACCTKEY003" }; foreach (var k in keys) { store.SaveAcc(k, $"jwt.for.{k}"); } foreach (var k in keys) { store.LoadAcc(k).ShouldBe($"jwt.for.{k}"); } } // Test 25 [Fact] public void DirJwtStore_EmptyKey_ReturnsError() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); // LoadAcc with key shorter than 2 chars should throw Should.Throw(() => store.LoadAcc("")); // SaveAcc with key shorter than 2 chars should throw Should.Throw(() => store.SaveAcc("", "some.jwt")); } // Test 26 [Fact] public void DirJwtStore_MissingKey_ReturnsError() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); Should.Throw(() => store.LoadAcc("NONEXISTENT_KEY")); } // Test 27 [Fact] public void DirJwtStore_Pack_ContainsSavedJwts() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); store.SaveAcc("ACCTKEYAAA", "jwt1.data.sig"); store.SaveAcc("ACCTKEYBBB", "jwt2.data.sig"); var packed = store.Pack(-1); packed.ShouldContain("ACCTKEYAAA|jwt1.data.sig"); packed.ShouldContain("ACCTKEYBBB|jwt2.data.sig"); } // Test 28 [Fact] public void DirJwtStore_Merge_AddsNewEntries() { var dir = MakeTempDir(); using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); // Pack format: key|jwt lines separated by newline var packData = "ACCTKEYMERGE|merged.jwt.value"; store.Merge(packData); var loaded = store.LoadAcc("ACCTKEYMERGE"); loaded.ShouldBe("merged.jwt.value"); } // Test 29 [Fact] public void DirJwtStore_ReadOnly_Prevents_Write() { var dir = MakeTempDir(); // Write a file first so the dir is valid var writeable = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false); writeable.SaveAcc("ACCTKEYRO", "original.jwt"); writeable.Dispose(); // Open as immutable using var readOnly = DirJwtStore.NewImmutableDirJwtStore(dir, shard: false); readOnly.IsReadOnly().ShouldBeTrue(); Should.Throw(() => readOnly.SaveAcc("ACCTKEYRO", "new.jwt")); } } // ========================================================================= // MemoryAccountResolver Tests // ========================================================================= [Collection("AccountTests")] public sealed class MemoryAccountResolverTests { // Test 30 [Fact] public async Task MemoryAccountResolver_StoreAndFetch_Roundtrip() { var resolver = new MemoryAccountResolver(); const string key = "MYACCOUNTKEY"; const string jwt = "header.payload.sig"; await resolver.StoreAsync(key, jwt); var fetched = await resolver.FetchAsync(key); fetched.ShouldBe(jwt); } // Test 31 [Fact] public async Task MemoryAccountResolver_Fetch_MissingKey_Throws() { var resolver = new MemoryAccountResolver(); await Should.ThrowAsync( () => resolver.FetchAsync("DOESNOTEXIST")); } // Test 32 [Fact] public void MemoryAccountResolver_IsReadOnly_ReturnsFalse() { var resolver = new MemoryAccountResolver(); resolver.IsReadOnly().ShouldBeFalse(); } } // ========================================================================= // UrlAccountResolver Tests // ========================================================================= [Collection("AccountTests")] public sealed class UrlAccountResolverTests { // Test 33 [Fact] public void UrlAccountResolver_NormalizesTrailingSlash() { // Two constructors: one with slash, one without. // We verify construction doesn't throw and the resolver is usable. // (We cannot inspect _url directly since it's private, but we can // infer correctness via IsReadOnly and lack of constructor exception.) var resolverNoSlash = new UrlAccountResolver("http://localhost:9090"); var resolverWithSlash = new UrlAccountResolver("http://localhost:9090/"); // Both should construct without error and have the same observable behaviour. resolverNoSlash.IsReadOnly().ShouldBeTrue(); resolverWithSlash.IsReadOnly().ShouldBeTrue(); } // Test 34 [Fact] public void UrlAccountResolver_IsReadOnly_ReturnsTrue() { var resolver = new UrlAccountResolver("http://localhost:9090"); resolver.IsReadOnly().ShouldBeTrue(); } }