E4: AccountImportExport with DFS cycle detection for service imports, RemoveServiceImport/RemoveStreamImport, and ValidateImport authorization. E5: AccountLimits record with MaxStorage/MaxConsumers/MaxAckPending, TryReserveConsumer/ReleaseConsumer, TrackStorageDelta on Account. 20 new tests, all passing.
212 lines
6.4 KiB
C#
212 lines
6.4 KiB
C#
// Tests for account import/export cycle detection.
|
|
// Go reference: accounts_test.go TestAccountImportCycleDetection.
|
|
|
|
using NATS.Server.Auth;
|
|
using NATS.Server.Imports;
|
|
|
|
namespace NATS.Server.Tests.Auth;
|
|
|
|
public class AccountImportExportTests
|
|
{
|
|
private static Account CreateAccount(string name) => new(name);
|
|
|
|
private static void SetupServiceExport(Account exporter, string subject, IEnumerable<Account>? approved = null)
|
|
{
|
|
exporter.AddServiceExport(subject, ServiceResponseType.Singleton, approved);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddServiceImport_NoCycle_Succeeds()
|
|
{
|
|
// A exports "svc.foo", B imports from A — no cycle
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
SetupServiceExport(a, "svc.foo"); // public export (no approved list)
|
|
|
|
var import = b.AddServiceImport(a, "svc.foo", "svc.foo");
|
|
|
|
import.ShouldNotBeNull();
|
|
import.DestinationAccount.Name.ShouldBe("A");
|
|
import.From.ShouldBe("svc.foo");
|
|
b.Imports.Services.ShouldContainKey("svc.foo");
|
|
}
|
|
|
|
[Fact]
|
|
public void AddServiceImport_DirectCycle_Throws()
|
|
{
|
|
// A exports "svc.foo", B exports "svc.bar"
|
|
// B imports "svc.foo" from A (ok)
|
|
// A imports "svc.bar" from B — creates cycle A->B->A
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
SetupServiceExport(a, "svc.foo");
|
|
SetupServiceExport(b, "svc.bar");
|
|
|
|
b.AddServiceImport(a, "svc.foo", "svc.foo");
|
|
|
|
Should.Throw<InvalidOperationException>(() => a.AddServiceImport(b, "svc.bar", "svc.bar"))
|
|
.Message.ShouldContain("cycle");
|
|
}
|
|
|
|
[Fact]
|
|
public void AddServiceImport_IndirectCycle_A_B_C_A_Throws()
|
|
{
|
|
// A->B->C, then C->A creates indirect cycle
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
var c = CreateAccount("C");
|
|
|
|
SetupServiceExport(a, "svc.a");
|
|
SetupServiceExport(b, "svc.b");
|
|
SetupServiceExport(c, "svc.c");
|
|
|
|
// B imports from A
|
|
b.AddServiceImport(a, "svc.a", "svc.a");
|
|
// C imports from B
|
|
c.AddServiceImport(b, "svc.b", "svc.b");
|
|
// A imports from C — would create C->B->A->C cycle
|
|
Should.Throw<InvalidOperationException>(() => a.AddServiceImport(c, "svc.c", "svc.c"))
|
|
.Message.ShouldContain("cycle");
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectCycle_NoCycle_ReturnsFalse()
|
|
{
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
var c = CreateAccount("C");
|
|
|
|
SetupServiceExport(a, "svc.a");
|
|
SetupServiceExport(b, "svc.b");
|
|
|
|
// A imports from B, B imports from C — linear chain, no cycle back to A
|
|
// For this test we manually add imports without cycle check via ImportMap
|
|
b.Imports.AddServiceImport(new ServiceImport
|
|
{
|
|
DestinationAccount = a,
|
|
From = "svc.a",
|
|
To = "svc.a",
|
|
});
|
|
|
|
// Check: does following imports from A lead back to C? No.
|
|
AccountImportExport.DetectCycle(a, c).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectCycle_DirectCycle_ReturnsTrue()
|
|
{
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
// A has import pointing to B
|
|
a.Imports.AddServiceImport(new ServiceImport
|
|
{
|
|
DestinationAccount = b,
|
|
From = "svc.x",
|
|
To = "svc.x",
|
|
});
|
|
|
|
// Does following from A lead to B? Yes.
|
|
AccountImportExport.DetectCycle(a, b).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectCycle_IndirectCycle_ReturnsTrue()
|
|
{
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
var c = CreateAccount("C");
|
|
|
|
// A -> B -> C (imports)
|
|
a.Imports.AddServiceImport(new ServiceImport
|
|
{
|
|
DestinationAccount = b,
|
|
From = "svc.1",
|
|
To = "svc.1",
|
|
});
|
|
b.Imports.AddServiceImport(new ServiceImport
|
|
{
|
|
DestinationAccount = c,
|
|
From = "svc.2",
|
|
To = "svc.2",
|
|
});
|
|
|
|
// Does following from A lead to C? Yes, via B.
|
|
AccountImportExport.DetectCycle(a, c).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveServiceImport_ExistingImport_Succeeds()
|
|
{
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
SetupServiceExport(a, "svc.foo");
|
|
b.AddServiceImport(a, "svc.foo", "svc.foo");
|
|
|
|
b.Imports.Services.ShouldContainKey("svc.foo");
|
|
|
|
b.RemoveServiceImport("svc.foo").ShouldBeTrue();
|
|
b.Imports.Services.ShouldNotContainKey("svc.foo");
|
|
|
|
// Removing again returns false
|
|
b.RemoveServiceImport("svc.foo").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveStreamImport_ExistingImport_Succeeds()
|
|
{
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
a.AddStreamExport("stream.data", null); // public
|
|
b.AddStreamImport(a, "stream.data", "imported.data");
|
|
|
|
b.Imports.Streams.Count.ShouldBe(1);
|
|
|
|
b.RemoveStreamImport("stream.data").ShouldBeTrue();
|
|
b.Imports.Streams.Count.ShouldBe(0);
|
|
|
|
// Removing again returns false
|
|
b.RemoveStreamImport("stream.data").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateImport_UnauthorizedAccount_Throws()
|
|
{
|
|
var exporter = CreateAccount("Exporter");
|
|
var importer = CreateAccount("Importer");
|
|
var approved = CreateAccount("Approved");
|
|
|
|
// Export only approves "Approved" account, not "Importer"
|
|
SetupServiceExport(exporter, "svc.restricted", [approved]);
|
|
|
|
Should.Throw<UnauthorizedAccessException>(
|
|
() => AccountImportExport.ValidateImport(importer, exporter, "svc.restricted"))
|
|
.Message.ShouldContain("not authorized");
|
|
}
|
|
|
|
[Fact]
|
|
public void AddStreamImport_NoCycleCheck_Succeeds()
|
|
{
|
|
// Stream imports do not require cycle detection (unlike service imports).
|
|
// Even with a "circular" stream import topology, it should succeed.
|
|
var a = CreateAccount("A");
|
|
var b = CreateAccount("B");
|
|
|
|
a.AddStreamExport("stream.a", null);
|
|
b.AddStreamExport("stream.b", null);
|
|
|
|
// B imports stream from A
|
|
b.AddStreamImport(a, "stream.a", "imported.a");
|
|
|
|
// A imports stream from B — no cycle check for streams
|
|
a.AddStreamImport(b, "stream.b", "imported.b");
|
|
|
|
a.Imports.Streams.Count.ShouldBe(1);
|
|
b.Imports.Streams.Count.ShouldBe(1);
|
|
}
|
|
}
|