using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration; /// /// Security register item 3 (DI role-scoping pin). IMeshPskProvider and /// IMeshCommandDialer are central's per-cluster key lookup + outbound command dialer — /// a driver-only node holds exactly its own MeshTransport:GrpcPsk and never dials /// another node, so these two singletons must be registered ONLY inside one of Program.cs's /// if (hasAdmin) blocks. Nothing in the language enforces that placement; this is a /// source scan so a refactor that hoists either registration out of its block fails loudly /// here instead of silently widening the mesh trust boundary to driver-only nodes. /// /// /// Program.cs gates several unrelated concerns behind repeated if (hasAdmin) checks /// (auth/AdminUI mounting, health probes, gRPC endpoint mapping, ...), so this pin cannot /// assume there is only one such block in the file — it must find the SPECIFIC block that /// contains each registration and prove that containment is unambiguous. One of the six /// markers (the ConfigDb gate) is a braceless single-statement if; the scanner handles /// that shape explicitly rather than scanning forward for a brace that belongs to unrelated /// code (see ). The scanner is also NOT string/comment /// aware — a literal {/}/; inside a string or comment inside a scanned /// span would misscan; it survives today only because no such span contains one, and the /// implausible-length tripwire below is the cheap backstop for exactly this class of mistake. /// [Trait("Category", "Unit")] public sealed class MeshRegistrationRoleScopingTests { private const string PskRegistration = "AddSingleton"; private const string DialerRegistration = "AddSingleton"; private const string HasAdminMarker = "if (hasAdmin)"; // The real hasAdmin blocks in Program.cs top out around 40 lines (the mesh-registration block // itself). 150 leaves ample margin for legitimate growth while still catching a mis-scan: the // phantom-block defect this tripwire was added for fabricated a ~290-line range by adopting an // unrelated if-block's brace. private const int MaxPlausibleBlockLines = 150; private static string ReadProgramSource() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) dir = dir.Parent; dir.ShouldNotBeNull("could not locate the repository root (no ZB.MOM.WW.OtOpcUa.slnx above the test binary)"); return File.ReadAllText(Path.Combine( dir!.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs")); } private static int CountOccurrences(string haystack, string needle) { var count = 0; var index = 0; while ((index = haystack.IndexOf(needle, index, StringComparison.Ordinal)) >= 0) { count++; index += needle.Length; } return count; } /// /// Scans every if (hasAdmin) occurrence in the file (not a regex over the block /// body — the body is free-form C# and a regex would be fragile to reformatting) and /// returns each one's body as a (start, end) character range. C# permits an if with /// no braces at all when the body is a single statement (Program.cs's ConfigDb gate is /// exactly this shape), so each occurrence is classified by what actually follows it: /// private static List<(int Start, int End)> FindHasAdminBlockRanges(string source) { var ranges = new List<(int Start, int End)>(); var searchIndex = 0; while (true) { var markerIndex = source.IndexOf(HasAdminMarker, searchIndex, StringComparison.Ordinal); if (markerIndex < 0) break; var bodyStart = markerIndex + HasAdminMarker.Length; while (bodyStart < source.Length && char.IsWhiteSpace(source[bodyStart])) bodyStart++; bodyStart.ShouldBeLessThan(source.Length, $"'{HasAdminMarker}' at offset {markerIndex} has no body — reached end of file."); (int Start, int End) range; if (source[bodyStart] == '{') range = ScanBracedBlock(source, bodyStart, markerIndex); else // NOT a fallback IndexOf('{', ...) scan: the next brace in the file belongs to // whatever code comes after this statement, not to this if — adopting it fabricates // a phantom block spanning unrelated code (the exact defect this scanner exists to // catch when it happens to a REAL registration). range = ScanBracelessStatement(source, bodyStart, markerIndex); var lineCount = 1 + CountOccurrences(source.Substring(range.Start, range.End - range.Start + 1), "\n"); lineCount.ShouldBeLessThanOrEqualTo(MaxPlausibleBlockLines, $"the scanned '{HasAdminMarker}' body starting at offset {range.Start} spans {lineCount} " + "lines, which is implausibly long for one of this file's admin-role gates — almost " + "certainly a mis-scan (e.g. a braceless if adopting an unrelated block's brace) rather " + "than a real block. Do not trust this range; fix the scanner instead."); ranges.Add(range); searchIndex = range.End + 1; } return ranges; } private static (int Start, int End) ScanBracedBlock(string source, int openBraceIndex, int markerIndex) { var depth = 0; var closeBraceIndex = -1; for (var i = openBraceIndex; i < source.Length; i++) { if (source[i] == '{') depth++; else if (source[i] == '}') { depth--; if (depth == 0) { closeBraceIndex = i; break; } } } closeBraceIndex.ShouldBeGreaterThan(-1, $"brace-matching for the '{HasAdminMarker}' block starting at offset {markerIndex} never closed."); return (openBraceIndex, closeBraceIndex); } /// /// Bounds a braceless single-statement if body to its terminating ;, tracking /// paren/brace depth so a semicolon nested inside the statement itself (e.g. a lambda or /// object initializer in the call's arguments) does not end the scan early. /// private static (int Start, int End) ScanBracelessStatement(string source, int statementStart, int markerIndex) { var depth = 0; var semicolonIndex = -1; for (var i = statementStart; i < source.Length; i++) { var c = source[i]; if (c is '(' or '{') depth++; else if (c is ')' or '}') depth--; else if (c == ';' && depth == 0) { semicolonIndex = i; break; } } semicolonIndex.ShouldBeGreaterThan(-1, $"the braceless '{HasAdminMarker}' single-statement body starting at offset {markerIndex} " + "never terminated with ';'."); return (statementStart, semicolonIndex); } /// /// Locates the single if (hasAdmin) block that contains . /// Fails loudly — rather than silently picking the first match — if the registration is /// missing entirely, sits outside every admin-role block, or (should a future edit make two /// blocks both contain matching text) the containing block is ambiguous. Any of those is a /// reason to stop and re-examine the pin, not to guess. /// private static string ExtractContainingHasAdminBlock(string source, string registration, string whyItMatters) { var ranges = FindHasAdminBlockRanges(source); ranges.ShouldNotBeEmpty( $"Program.cs no longer contains any '{HasAdminMarker}' block — the admin-role gate this " + $"pin relies on is gone. {whyItMatters}"); var registrationIndex = source.IndexOf(registration, StringComparison.Ordinal); registrationIndex.ShouldBeGreaterThan(-1, $"'{registration}' was not found anywhere in Program.cs. {whyItMatters}"); var containingBlocks = ranges .Where(r => registrationIndex >= r.Start && registrationIndex <= r.End) .ToList(); containingBlocks.ShouldNotBeEmpty( $"'{registration}' is registered OUTSIDE every '{HasAdminMarker}' block. {whyItMatters}"); containingBlocks.Count.ShouldBe(1, $"'{registration}' falls inside {containingBlocks.Count} '{HasAdminMarker}' blocks, so " + "which one actually gates it is ambiguous — extend this pin to disambiguate rather than " + $"trusting the first match. {whyItMatters}"); var (start, end) = containingBlocks[0]; return source.Substring(start, end - start + 1); } [Fact] public void MeshPskProvider_IsRegisteredExactlyOnce_InsideAnUnambiguousHasAdminBlock() { const string why = "A driver-only node must not carry central's per-cluster key lookup, which it can never " + "legitimately use."; var source = ReadProgramSource(); // Global uniqueness first: if this text appeared twice, block-containment below could pass // even though a second, unguarded copy exists elsewhere in the file. CountOccurrences(source, PskRegistration).ShouldBe(1, $"IMeshPskProvider must be registered exactly once in Program.cs. {why}"); var block = ExtractContainingHasAdminBlock(source, PskRegistration, why); block.ShouldContain(PskRegistration, Case.Sensitive, $"IMeshPskProvider must be registered inside 'if (hasAdmin)'. {why}"); } [Fact] public void MeshGrpcCommandDialer_IsRegisteredExactlyOnce_InsideAnUnambiguousHasAdminBlock() { const string why = "A driver-only node never dials another node and must not carry central's outbound mesh " + "command dialer."; var source = ReadProgramSource(); CountOccurrences(source, DialerRegistration).ShouldBe(1, $"IMeshCommandDialer must be registered exactly once in Program.cs. {why}"); var block = ExtractContainingHasAdminBlock(source, DialerRegistration, why); block.ShouldContain(DialerRegistration, Case.Sensitive, $"IMeshCommandDialer must be registered inside 'if (hasAdmin)'. {why}"); } /// /// Pins the scanner's expected result shape against today's Program.cs — 5 markers, /// 4 braced blocks + the 1 braceless ConfigDb gate, so 5 scanned ranges (the data-plane /// key-store branch moved into DataPlaneAuthRegistration for arch-review 03/S18, taking /// one braced marker with it). Without this, a /// scanner bug that silently drops or merges a marker (as the pre-fix version did to the /// braceless marker — it fabricated a phantom range spanning an unrelated if-block and swallowed /// a real nested 'if (hasAdmin)' block inside it, yielding 5 ranges instead of 6) would not /// surface unless it happened to also break one of the two registration tests above. /// [Fact] public void HasAdminMarkerScan_MatchesTheKnownShapeOfProgramCs() { var source = ReadProgramSource(); CountOccurrences(source, HasAdminMarker).ShouldBe(5, "Program.cs's 'if (hasAdmin)' marker count changed. If a marker was added or removed, " + "update this pin — and check whether the new/removed occurrence is braced or braceless — " + "rather than letting the scanner silently mis-shape its result."); var ranges = FindHasAdminBlockRanges(source); ranges.Count.ShouldBe(5, "expected one scanned range per 'if (hasAdmin)' marker, including the braceless ConfigDb " + "gate. A mismatch means the scanner adopted an unrelated brace or otherwise merged/dropped " + "a marker instead of handling each one independently."); } }