// Tests for account import/export cycle detection. // Go reference: accounts_test.go TestAccountImportCycleDetection. using NATS.Server.Auth; using NATS.Server.Imports; namespace NATS.Server.Auth.Tests.Auth; public class AccountImportExportTests { private static Account CreateAccount(string name) => new(name); private static void SetupServiceExport(Account exporter, string subject, IEnumerable? 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(() => 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(() => 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( () => 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); } }