using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Xunit; namespace ytLive.Tests; /// /// Shared helpers for the source-level tests — the ones that assert on the text of a /// source file because the behaviour they guard is structural (a field confined to /// #if DEBUG, a type carrying no network capability, a build refusing to compile /// without an asset). Those are legitimate assertions: InstanceIsolationTests has /// carried one since 2026-08 and it caught a real hoisting bug. /// /// Use , not a file-wide string match. Three tests each grew /// their own File.ReadAllText + RepoFile pair, and two of them then failed on the /// file's own doc comment — the locator's DoesNotContain("Download") matched the sentence /// saying "nothing is downloaded", and the auth test's DoesNotContain("force-ssl") matched /// the comment explaining why force-ssl was removed. Both were fixed by parsing the literal out /// of the source and asserting on the value. That failure is not a one-off: a file is /// mostly prose, and the documentation is supposed to name the thing being banned. A /// grep-based ban asserts "this string never appears", which is a much stronger and much less /// useful claim than "this value is correct". /// /// Only the path-walking and the literal extraction live here. Tests that genuinely need to /// assert on structure — the #if DEBUG arm, the absence of a whole API — should still read /// the file themselves, because those claims are about code and cannot be reduced to a value. /// internal static class SourceAssert { /// Walks up from the test output directory to the repo root and returns the /// absolute path of the given repo-relative file. public static string RepoPath(params string[] parts) { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { var candidate = System.IO.Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray()); if (File.Exists(candidate)) return candidate; dir = dir.Parent; } throw new FileNotFoundException( $"could not locate {System.IO.Path.Combine(parts)} above {AppContext.BaseDirectory}"); } public static string Text(params string[] parts) => File.ReadAllText(RepoPath(parts)); /// Extracts the value of a C# string constant or const field declared in the /// given source file, e.g. Literal("Scope", "Services", "YouTubeAuthService.cs") for /// private const string Scope = "…";. Whitespace and the trailing semicolon are not /// part of the value. The path parts are repo-relative and may span directories — the /// member name comes first so a wrong path can never be mistaken for a member name. /// public static string Literal(string memberName, params string[] parts) { var source = Text(parts); var match = Regex.Match( source, $@"(?:const|static readonly|readonly)\s+string\s+{Regex.Escape(memberName)}\s*=\s*""([^""]*)""\s*;"); Assert.True(match.Success, $"could not find a string constant named '{memberName}' in {System.IO.Path.Combine(parts)} — " + "if it was renamed or its declaration reshaped, update this test rather than " + "loosening the assertion."); return match.Groups[1].Value; } /// The scopes requested in a space-separated OAuth scope const, split into /// individual scope URIs so a test can assert on the SET rather than on one string. public static string[] Scopes(string memberName, params string[] parts) => Literal(memberName, parts) .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); }