Add StreamImportFormsCycle DFS method to Account plus GetStreamImportSources and HasStreamImportFrom helpers. Add GetStreamImportSourceAccounts to ImportMap. 10 tests cover direct, indirect, self, diamond, and empty import scenarios.
176 lines
6.2 KiB
C#
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.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();
|
|
}
|
|
}
|