using AutopilotMonitor.Functions.Functions.Bootstrap; using AutopilotMonitor.Functions.Middleware; using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Shared; namespace AutopilotMonitor.Functions.Tests; /// /// Seam tests for GET /api/bootstrap/go/{code} (GetBootstrapScriptFunction) — /// the OOBE bootstrap script endpoint reached by customers as /// irm https://go.autopilotmonitor.com/{code} | iex. Following the project /// convention (see AppInsightsQueryFunctionTests), the HTTP entrypoint itself is not /// faked; the compilable seams — code-format gate, policy-catalog registration, and /// the no-store cache classification — are pinned here. Script generation and value /// validation have their own suites. /// public class GetBootstrapScriptFunctionTests { private const string Route = "/api/bootstrap/go/abcd"; // ── Code-format gate: 4-10 alphanumeric, checked before any service call. // Table ported from the vitest suite of the original Next.js route. ── [Theory] [InlineData("")] [InlineData("abc")] // too short [InlineData("abcdefghijk")] // too long [InlineData("ab cd")] [InlineData("../etc")] [InlineData("ab%2Fcd")] [InlineData("abc$")] [InlineData("ab'cd")] [InlineData("ab.cd")] public void Rejects_invalid_code_formats(string code) { Assert.False(GetBootstrapScriptFunction.IsValidCodeFormat(code)); } [Theory] [InlineData("abcd")] [InlineData("ABC123")] [InlineData("a1b2c3d4e5")] public void Accepts_valid_code_formats(string code) { Assert.True(GetBootstrapScriptFunction.IsValidCodeFormat(code)); } // ── Catalog registration: anonymous (OOBE devices have no JWT, no cert), // JWT-exempt via the catalog-derived rule. Unregistered routes fail closed, // so this pins reachability. ── [Fact] public void Route_is_registered_as_PublicAnonymous() { var entry = EndpointAccessPolicyCatalog.FindPolicy("GET", Route); Assert.NotNull(entry); Assert.Equal(EndpointPolicy.PublicAnonymous, entry!.Policy); } [Fact] public void Route_is_jwt_exempt() { Assert.True(AuthenticationMiddleware.SkipsJwtValidation("GET", Route)); } // ── The script body inlines the bearer token — must never be cached. ── [Fact] public void Route_is_classified_no_store() { Assert.True(NoStoreCacheMiddleware.IsSensitive(Route)); } // ── AFD-aware rate-limit extraction: X-Azure-ClientIP is honored only when // X-Azure-FDID carries EXACTLY our profile ID. Multi-value headers mean // someone appended to what AFD set — untrusted, fail closed. ── private const string Fdid = "3490f60d-3639-495f-902e-681b9324d2ba"; [Theory] [InlineData(new[] { Fdid }, true)] [InlineData(new[] { "3490F60D-3639-495F-902E-681B9324D2BA" }, true)] // case-insensitive [InlineData(new[] { " " + Fdid + " " }, true)] // trimmed [InlineData(new[] { "other-profile-id" }, false)] [InlineData(new[] { Fdid, Fdid }, false)] // two headers [InlineData(new[] { Fdid + "," + Fdid }, false)] // comma-appended [InlineData(new[] { "spoofed," + Fdid }, false)] [InlineData(new string[0], false)] [InlineData(new[] { "" }, false)] public void FdidMatches_requires_exactly_our_profile_id(string[] headerValues, bool expected) { Assert.Equal(expected, ClientIpExtractor.FdidMatches(headerValues, Fdid)); } } /// /// Pins the customer-facing bootstrap URL produced by CreateBootstrapSessionFunction — /// previously unguarded, which made a silent origin change possible. The literal here /// is a deliberate independent oracle per the url-registry doctrine (test files are /// excluded from HardcodedUrlGuardTests). /// public class CreateBootstrapSessionUrlShapeTests { [Fact] public void BootstrapGoBaseUrl_is_the_go_domain() { Assert.Equal("https://go.autopilotmonitor.com", Constants.BootstrapGoBaseUrl); } // ── Notification session deep link (Teams/Slack/webhook "Open session"): the // query-string shape the static-export portal understands. Literal is a // deliberate independent oracle; the legacy /sessions/{id} path shape is // rewritten client-side by the portal forever. ── [Fact] public void PortalSessionUrl_is_the_query_string_shape() { Assert.Equal( "https://portal.autopilotmonitor.com/sessions?id=abc-123", Constants.PortalSessionUrl("abc-123")); } [Fact] public void PortalSessionUrl_escapes_the_session_id() { Assert.Equal( "https://portal.autopilotmonitor.com/sessions?id=a%26b%3Dc", Constants.PortalSessionUrl("a&b=c")); } [Fact] public void CreateBootstrapSession_builds_the_url_from_the_go_constant() { // Source-level pin (the URL expression sits inside the HTTP entrypoint with no // injectable seam): the create function must derive BootstrapUrl from // BootstrapGoBaseUrl + "/{shortCode}" — not from WebsiteBaseUrl, and without a // path segment between host and code. var repoRoot = FindRepoRoot(); var source = File.ReadAllText(Path.Combine( repoRoot, "src", "Backend", "AutopilotMonitor.Functions", "Functions", "Bootstrap", "CreateBootstrapSessionFunction.cs")); Assert.Contains("$\"{Constants.BootstrapGoBaseUrl}/{session.ShortCode}\"", source); Assert.DoesNotContain("WebsiteBaseUrl}/go/", source); } private static string FindRepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AutopilotMonitor.sln"))) { dir = dir.Parent; } Assert.NotNull(dir); return dir!.FullName; } }