// Tests for stream import cycle detection via DFS on Account. // Go reference: accounts_test.go — TestAccountStreamImportCycles (accounts.go:1627 streamImportFormsCycle). using NATS.Server.Auth; namespace NATS.Server.Auth.Tests.Auth; public class StreamImportCycleTests { private static Account CreateAccount(string name) => new(name); /// /// Sets up a public stream export on for /// and then adds a stream import on from . /// private static void SetupStreamImport(Account importer, Account exporter, string subject) { exporter.AddStreamExport(subject, approved: null); // public export importer.AddStreamImport(exporter, subject, subject); } // 1. No cycle when the proposed source has no imports leading back to this account. // A imports from B; checking whether B can import from C — no path C→A exists. [Fact] public void StreamImportFormsCycle_NoCycle_ReturnsFalse() { // Go ref: accounts.go streamImportFormsCycle var a = CreateAccount("A"); var b = CreateAccount("B"); var c = CreateAccount("C"); SetupStreamImport(a, b, "events.>"); // A imports from B c.AddStreamExport("other.>", approved: null); // B importing from C: does C→...→A exist? No. a.StreamImportFormsCycle(c).ShouldBeFalse(); } // 2. Direct cycle: A already imports from B; proposing B imports from A = cycle. [Fact] public void StreamImportFormsCycle_DirectCycle_ReturnsTrue() { // Go ref: accounts.go streamImportFormsCycle var a = CreateAccount("A"); var b = CreateAccount("B"); SetupStreamImport(a, b, "stream.>"); // A imports from B // Now check: would A importing from B (again, or B's perspective) form a cycle? // We ask account B: does proposing A as source form a cycle? // i.e. b.StreamImportFormsCycle(a) — does a chain from A lead back to B? // A imports from B, so A→B, meaning following A's imports we reach B. Cycle confirmed. b.StreamImportFormsCycle(a).ShouldBeTrue(); } // 3. Indirect cycle: A→B→C; proposing C import from A would create C→A→B→C. [Fact] public void StreamImportFormsCycle_IndirectCycle_ReturnsTrue() { // Go ref: accounts.go checkStreamImportsForCycles var a = CreateAccount("A"); var b = CreateAccount("B"); var c = CreateAccount("C"); SetupStreamImport(a, b, "s.>"); // A imports from B SetupStreamImport(b, c, "t.>"); // B imports from C // Would C importing from A form a cycle? Path: A imports from B, B imports from C → cycle. c.StreamImportFormsCycle(a).ShouldBeTrue(); } // 4. Self-import: A importing from A is always a cycle. [Fact] public void StreamImportFormsCycle_SelfImport_ReturnsTrue() { // Go ref: accounts.go streamImportFormsCycle — proposedSource == this var a = CreateAccount("A"); a.StreamImportFormsCycle(a).ShouldBeTrue(); } // 5. Account with no imports at all — no cycle possible. [Fact] public void StreamImportFormsCycle_NoImports_ReturnsFalse() { // Go ref: accounts.go streamImportFormsCycle — empty imports.streams var a = CreateAccount("A"); var b = CreateAccount("B"); // Neither account has any stream imports; proposing B as source for A is safe. a.StreamImportFormsCycle(b).ShouldBeFalse(); } // 6. Diamond topology: A→B, A→C, B→D, C→D — no cycle, just shared descendant. [Fact] public void StreamImportFormsCycle_DiamondNoCycle_ReturnsFalse() { // Go ref: accounts.go checkStreamImportsForCycles — visited set prevents false positives var a = CreateAccount("A"); var b = CreateAccount("B"); var c = CreateAccount("C"); var d = CreateAccount("D"); SetupStreamImport(a, b, "b.>"); // A imports from B SetupStreamImport(a, c, "c.>"); // A imports from C SetupStreamImport(b, d, "d1.>"); // B imports from D SetupStreamImport(c, d, "d2.>"); // C imports from D // Proposing D import from A: does A→...→D path exist? Yes (via B and C). d.StreamImportFormsCycle(a).ShouldBeTrue(); // Proposing E (new account) import from D: D has no imports, so no cycle. var e = CreateAccount("E"); e.StreamImportFormsCycle(d).ShouldBeFalse(); } // 7. GetStreamImportSources returns names of source accounts. [Fact] public void GetStreamImportSources_ReturnsSourceNames() { // Go ref: accounts.go imports.streams acc field var a = CreateAccount("A"); var b = CreateAccount("B"); var c = CreateAccount("C"); SetupStreamImport(a, b, "x.>"); SetupStreamImport(a, c, "y.>"); var sources = a.GetStreamImportSources(); sources.Count.ShouldBe(2); sources.ShouldContain("B"); sources.ShouldContain("C"); } // 8. GetStreamImportSources returns empty list when no imports exist. [Fact] public void GetStreamImportSources_Empty_ReturnsEmpty() { // Go ref: accounts.go imports.streams — empty slice var a = CreateAccount("A"); var sources = a.GetStreamImportSources(); sources.ShouldBeEmpty(); } // 9. HasStreamImportFrom returns true when a matching import exists. [Fact] public void HasStreamImportFrom_True() { // Go ref: accounts.go imports.streams — acc.Name lookup var a = CreateAccount("A"); var b = CreateAccount("B"); SetupStreamImport(a, b, "events.>"); a.HasStreamImportFrom("B").ShouldBeTrue(); } // 10. HasStreamImportFrom returns false when no import from that account exists. [Fact] public void HasStreamImportFrom_False() { // Go ref: accounts.go imports.streams — acc.Name lookup miss var a = CreateAccount("A"); var b = CreateAccount("B"); var c = CreateAccount("C"); SetupStreamImport(a, b, "events.>"); a.HasStreamImportFrom("C").ShouldBeFalse(); a.HasStreamImportFrom(c.Name).ShouldBeFalse(); } }