Files
natsdotnet/tests/NATS.Server.Auth.Tests/Auth/StreamImportCycleTests.cs
Joseph Doherty 36b9dfa654 refactor: extract NATS.Server.Auth.Tests project
Move 50 auth/accounts/permissions/JWT/NKey test files from
NATS.Server.Tests into a dedicated NATS.Server.Auth.Tests project.
Update namespaces, replace private GetFreePort/ReadUntilAsync helpers
with TestUtilities calls, replace Task.Delay with TaskCompletionSource
in test doubles, and add InternalsVisibleTo.

690 tests pass.
2026-03-12 15:54:07 -04:00

176 lines
6.2 KiB
C#

// 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);
/// <summary>
/// Sets up a public stream export on <paramref name="exporter"/> for <paramref name="subject"/>
/// and then adds a stream import on <paramref name="importer"/> from <paramref name="exporter"/>.
/// </summary>
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();
}
}