Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/ValidateBootstrapCodeResponseShapeTests.cs
T
Oliver Kieselbach e0ad51b965 refactor(urls): one guard-enforced URL registry per component, agent downloads move to the alias
Every well-known host -- own subdomains, both blob accounts, the API origin,
Graph, Entra login -- now lives in exactly one registry per component:
Constants.cs (*BaseUrl) for C#, utils/config.ts for the web app, src/config.ts
for the MCP server. lib/hostRouting.ts derives its hostnames from the registry
and middleware.ts imports from it, retiring what turned out to be a fourth
independent copy of the www/portal host pair.

Three new guard tests (backend xUnit, web vitest, MCP vitest) scan non-test
source and fail on any enforced host outside its registry. This is the lesson
of the EU cutover: the registry existed then too, but LatestVersionsService
(feeds the portal's version badge) and ValidateBootstrapCodeFunction carried
their own literal copies of the legacy blob host and were missed -- kept
working only by the fail-soft legacy mirror in build-agent.yml, whose silent
failure would have pinned badge and self-update to stale bits with a green
release. Both guards proved themselves on their first run, flagging the
middleware host copy and a literal in an MCP tool description. Test files stay
exempt on purpose: literals there are independent oracles, and
ValidateBootstrapCodeResponseShapeTests now pins the full alias URL against
the Constants-built one so registry changes must be acknowledged consciously.

The two legacy readers plus the agent SelfUpdater now use
Constants.AgentDownloadBaseUrl (download.autopilotmonitor.com/agent); the
alias was verified live for version.json, version-v2.json and both ZIPs
before the switch. The portal's bootstrap allow-list accepts the alias AND
the legacy host (AGENT_DOWNLOAD_HOSTNAMES) for already-deployed customer
scripts, which imposes a deploy order: web before or with backend. The
legacy keepalive probes and the release mirror stay untouched -- they exist
for exactly this transition. Also: the bootstrap short-link now targets www
directly instead of riding the registrar's apex 301, and the welcome-mail
docs button points at docs. instead of bouncing through www./docs.

Suites: backend 3194, agent core 1925, DecisionCore 474, web 582, MCP 449 --
all green; next build verified (it, not tsc, proves the next.config.ts
registry import). Docs: docs/url-registry.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 08:37:42 +02:00

91 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Reflection;
using System.Text.RegularExpressions;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Contract tests for <see cref="ValidateBootstrapCodeResponse"/>, the DTO returned
/// by <c>GET /api/bootstrap/validate/{code}</c>.
///
/// The values in this response are interpolated into a PowerShell bootstrap
/// script that runs as SYSTEM during Autopilot OOBE, and the Next.js frontend
/// at <c>src/Web/autopilot-monitor-web/lib/bootstrapValidation.ts</c> enforces
/// a very strict shape (GUID tenantId+token, locked-down AgentDownloadUrl, etc.).
///
/// These tests mirror the frontend's invariants so that a backend rename or
/// change to <c>AgentDownloadUrl</c> is caught in CI — not during OOBE rollout.
/// </summary>
public class ValidateBootstrapCodeResponseShapeTests
{
/// <summary>
/// Mirrors the frontend regex at <c>bootstrapValidation.ts</c> (lines ~119–125).
/// </summary>
private static readonly Regex AgentDownloadUrlPath =
new(@"^/agent/[A-Za-z0-9_-][A-Za-z0-9._-]{0,79}\.zip$", RegexOptions.Compiled);
/// <summary>
/// The response DTO must expose exactly these public instance properties with
/// these types. Any rename or removal breaks the frontend validator silently.
/// </summary>
[Fact]
public void ValidateBootstrapCodeResponse_HasExpectedPublicProperties()
{
var expected = new Dictionary<string, Type>
{
["Success"] = typeof(bool),
["TenantId"] = typeof(string),
["Token"] = typeof(string),
["AgentDownloadUrl"] = typeof(string),
["ExpiresAt"] = typeof(DateTime),
["Message"] = typeof(string),
};
var actual = typeof(ValidateBootstrapCodeResponse)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(p => p.Name, p => p.PropertyType);
// Missing / renamed properties
var missing = expected.Keys.Except(actual.Keys).ToList();
Assert.True(missing.Count == 0,
"ValidateBootstrapCodeResponse is missing expected properties: " + string.Join(", ", missing));
// Unexpected additions (forces a deliberate test update when the contract grows)
var extra = actual.Keys.Except(expected.Keys).ToList();
Assert.True(extra.Count == 0,
"ValidateBootstrapCodeResponse has unexpected extra properties (update frontend validator + this test): "
+ string.Join(", ", extra));
// Type mismatches
foreach (var kvp in expected)
{
Assert.Equal(kvp.Value, actual[kvp.Key]);
}
}
/// <summary>
/// The agent download URL served by <c>ValidateBootstrapCodeFunction</c> must
/// satisfy the frontend's host allow-list and path regex.
/// </summary>
[Fact]
public void AgentDownloadUrl_MatchesFrontendAllowList()
{
// Independent oracle, deliberately spelled out: if Constants or the function
// change the URL, this literal AND bootstrapValidation.ts (AGENT_DOWNLOAD_HOSTNAMES)
// must be updated consciously — that pairing is what keeps portal and backend in sync.
const string ExpectedAgentUrl =
"https://download.autopilotmonitor.com/agent/AutopilotMonitor-Agent.zip";
var built = $"{AutopilotMonitor.Shared.Constants.AgentDownloadBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}";
Assert.Equal(ExpectedAgentUrl, built);
Assert.True(Uri.TryCreate(built, UriKind.Absolute, out var uri),
"AgentDownloadUrl is not a well-formed absolute URI");
Assert.Equal(Uri.UriSchemeHttps, uri!.Scheme);
Assert.Equal("download.autopilotmonitor.com", uri.Host);
Assert.True(AgentDownloadUrlPath.IsMatch(uri.AbsolutePath),
$"AgentDownloadUrl path '{uri.AbsolutePath}' does not match the frontend allow-list regex");
}
}