mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
e0ad51b965
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>
91 lines
3.9 KiB
C#
91 lines
3.9 KiB
C#
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");
|
||
}
|
||
}
|