using System.Reflection;
using System.Text.RegularExpressions;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Tests;
///
/// Contract tests for , the DTO returned
/// by GET /api/bootstrap/validate/{code}.
///
/// 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 src/Web/autopilot-monitor-web/lib/bootstrapValidation.ts 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 AgentDownloadUrl is caught in CI — not during OOBE rollout.
///
public class ValidateBootstrapCodeResponseShapeTests
{
///
/// Mirrors the frontend regex at bootstrapValidation.ts (lines ~119–125).
///
private static readonly Regex AgentDownloadUrlPath =
new(@"^/agent/[A-Za-z0-9_-][A-Za-z0-9._-]{0,79}\.zip$", RegexOptions.Compiled);
///
/// The response DTO must expose exactly these public instance properties with
/// these types. Any rename or removal breaks the frontend validator silently.
///
[Fact]
public void ValidateBootstrapCodeResponse_HasExpectedPublicProperties()
{
var expected = new Dictionary
{
["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]);
}
}
///
/// The agent download URL served by ValidateBootstrapCodeFunction must
/// satisfy the frontend's host allow-list and path regex.
///
[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");
}
}