Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Accounts/AccountTests.cs

479 lines
13 KiB
C#

// 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<string> _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<Exception>(() => store.LoadAcc(""));
// SaveAcc with key shorter than 2 chars should throw
Should.Throw<Exception>(() => 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<FileNotFoundException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(
() => 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();
}
}