Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs
T
KyuubiYoru 41be7fbce5
quality-gate / quality (push) Successful in 2m37s
quality-gate / container (push) Failing after 1m40s
fix(ci): join service to runner network (#1)
2026-07-17 01:36:47 +02:00

294 lines
18 KiB
C#

using System.Reflection;
using System.Text.Json;
using System.Xml.Linq;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Operations;
namespace FinalFactory.Rendezvous.Tests.Release;
public sealed class ReleaseCompatibilityTests
{
[Fact]
public void CentralVersionsRuntimeWindowAndPublishedMatrixStayAligned()
{
string root = FindRepositoryRoot();
XDocument versions = XDocument.Load(Path.Combine(root, "eng/Versions.props"));
string releaseVersion = Property(versions, "RendezvousVersion");
int releaseMajor = int.Parse(Property(versions, "RendezvousMajorVersion"), System.Globalization.CultureInfo.InvariantCulture);
string[] numericVersion = releaseVersion.Split(['-', '+'], 2)[0].Split('.');
Assert.Equal(releaseMajor, int.Parse(numericVersion[0], System.Globalization.CultureInfo.InvariantCulture));
Assert.Equal(Property(versions, "RendezvousMinorVersion"), numericVersion[1]);
Assert.Equal(Property(versions, "RendezvousPatchVersion"), numericVersion[2]);
int httpVersion = int.Parse(Property(versions, "HttpContractVersion"), System.Globalization.CultureInfo.InvariantCulture);
int udpVersion = int.Parse(Property(versions, "UdpContractVersion"), System.Globalization.CultureInfo.InvariantCulture);
int ticketVersion = int.Parse(Property(versions, "ConnectionTicketFormatVersion"), System.Globalization.CultureInfo.InvariantCulture);
Assert.Equal(releaseVersion, typeof(RendezvousPublisherClient).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0]);
Assert.Equal(releaseVersion, typeof(ContractLimits).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0]);
Assert.Equal(ContractLimits.ContractVersion, httpVersion);
OperatorCompatibilityResponse runtime = ReleaseCompatibility.CreateResponse();
Assert.Equal(releaseVersion, runtime.ServerVersion);
Assert.Equal(Property(versions, "MinimumClientVersion"), runtime.MinimumClientVersion);
Assert.Equal(int.Parse(Property(versions, "MaximumClientMajorVersion"), System.Globalization.CultureInfo.InvariantCulture), runtime.MaximumClientMajorVersion);
Assert.Equal([httpVersion], runtime.HttpContractVersions);
Assert.Equal([udpVersion], runtime.UdpContractVersions);
Assert.Equal([ticketVersion], runtime.ConnectionTicketFormatVersions);
Assert.Equal(int.Parse(Property(versions, "LiteNetLibMajorVersion"), System.Globalization.CultureInfo.InvariantCulture), runtime.LiteNetLibMajorVersion);
Assert.Equal("exact-per-tenant", runtime.GameplayProtocolCompatibility);
using JsonDocument matrix = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "docs/releases/compatibility.json")));
JsonElement document = matrix.RootElement;
Assert.Equal(releaseVersion, document.GetProperty("release").GetString());
Assert.Equal(releaseVersion, document.GetProperty("packages").GetProperty("FinalFactory.Rendezvous.Client").GetString());
Assert.Equal(releaseVersion, document.GetProperty("packages").GetProperty("FinalFactory.Rendezvous.Contracts").GetString());
Assert.Equal(runtime.MinimumClientVersion, document.GetProperty("server").GetProperty("minimumClientVersion").GetString());
Assert.Equal(runtime.MaximumClientMajorVersion, document.GetProperty("server").GetProperty("maximumClientMajorVersion").GetInt32());
Assert.Equal(httpVersion, Assert.Single(document.GetProperty("contracts").GetProperty("http").EnumerateArray()).GetInt32());
Assert.Equal(udpVersion, Assert.Single(document.GetProperty("contracts").GetProperty("udp").EnumerateArray()).GetInt32());
Assert.Equal(ticketVersion, Assert.Single(document.GetProperty("contracts").GetProperty("connectionTicket").EnumerateArray()).GetInt32());
Assert.Equal(runtime.GameplayProtocolCompatibility, document.GetProperty("contracts").GetProperty("gameplay").GetString());
Assert.Equal("LiteNetLib", document.GetProperty("transport").GetProperty("package").GetString());
Assert.Equal(Property(versions, "LiteNetLibVersion"), document.GetProperty("transport").GetProperty("version").GetString());
Assert.Equal(runtime.LiteNetLibMajorVersion, document.GetProperty("transport").GetProperty("major").GetInt32());
foreach ((string consumer, string fixture) in new[]
{
("SpaceGame", "spacegame/SpaceGame.Rendezvous.Consumer.csproj"),
("Unscouted", "unscouted/Unscouted.Rendezvous.Consumer.csproj"),
})
{
XDocument fixtureProject = XDocument.Load(Path.Combine(root, "tests/consumers", fixture));
Assert.Equal(
fixtureProject.Descendants("TargetFramework").Single().Value,
document.GetProperty("consumers").GetProperty(consumer).GetString());
}
string snapshots = Path.Combine(root, $"tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v{releaseMajor}");
Assert.True(File.Exists(Path.Combine(snapshots, "client-public-api.txt")));
Assert.True(File.Exists(Path.Combine(snapshots, "contracts-public-api.txt")));
Assert.True(File.Exists(Path.Combine(root, $"docs/api/rendezvous-v{httpVersion}.json")));
}
[Fact]
public void ReleaseDefinitionIsImmutableTagOnlyAndDocumentsRequiredNotes()
{
string root = FindRepositoryRoot();
XDocument versions = XDocument.Load(Path.Combine(root, "eng/Versions.props"));
string releaseVersion = Property(versions, "RendezvousVersion");
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/release.yml"));
Assert.Contains("tags:", workflow, StringComparison.Ordinal);
Assert.Contains("RELEASE_TOKEN", workflow, StringComparison.Ordinal);
Assert.Contains("RELEASE_USERNAME", workflow, StringComparison.Ordinal);
Assert.Contains("COSIGN_PRIVATE_KEY", workflow, StringComparison.Ordinal);
Assert.DoesNotContain(":latest", workflow, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("skip-duplicate", workflow, StringComparison.OrdinalIgnoreCase);
Assert.Contains("check-compatibility.sh", workflow, StringComparison.Ordinal);
Assert.Contains("--platform linux/amd64", workflow, StringComparison.Ordinal);
Assert.Contains("ignore-unfixed: false", workflow, StringComparison.Ordinal);
Assert.Contains("format: spdx-json", workflow, StringComparison.Ordinal);
Assert.Contains("normalize-container-sbom", workflow, StringComparison.Ordinal);
Assert.Contains("finalize-release-candidate.sh", workflow, StringComparison.Ordinal);
Assert.Contains("verify-real-consumers.sh", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_RELEASE_BUILDER", workflow, StringComparison.Ordinal);
Assert.Contains("--image-id", workflow, StringComparison.Ordinal);
string publisher = File.ReadAllText(Path.Combine(root, "scripts/publish-release.sh"));
Assert.Contains("expected_image_id", publisher, StringComparison.Ordinal);
Assert.Contains("finalize-signing-ready-release.sh", publisher, StringComparison.Ordinal);
XDocument packages = XDocument.Load(Path.Combine(root, "Directory.Packages.props"));
XElement liteNetLib = Assert.Single(packages.Descendants("PackageVersion"), static item => (string?)item.Attribute("Include") == "LiteNetLib");
Assert.Equal("[$(LiteNetLibVersion)]", (string?)liteNetLib.Attribute("Version"));
string changelog = File.ReadAllText(Path.Combine(root, "CHANGELOG.md"));
Assert.Contains("### Compatibility", changelog, StringComparison.Ordinal);
Assert.Contains("### Security and configuration", changelog, StringComparison.Ordinal);
Assert.Contains("### Migration", changelog, StringComparison.Ordinal);
Assert.DoesNotContain($"{releaseVersion} - Unreleased", changelog, StringComparison.Ordinal);
foreach (string consumer in new[] { "spacegame", "unscouted" })
{
string consumerDirectory = Path.Combine(root, "tests/consumers", consumer);
string projectPath = Assert.Single(Directory.GetFiles(consumerDirectory, "*.csproj"));
XDocument consumerProject = XDocument.Load(projectPath);
XElement[] references = consumerProject.Descendants("PackageReference")
.Where(static item => ((string?)item.Attribute("Include"))?.StartsWith("FinalFactory.Rendezvous.", StringComparison.Ordinal) == true)
.ToArray();
Assert.Equal(2, references.Length);
Assert.All(references, static reference =>
Assert.Equal("[$(RendezvousPackageVersion)]", (string?)reference.Attribute("Version")));
Assert.Equal(
"false",
consumerProject.Descendants("RestorePackagesWithLockFile").Single().Value);
}
XDocument unscouted = XDocument.Load(Path.Combine(
root,
"tests/consumers/unscouted/Unscouted.Rendezvous.Consumer.csproj"));
XElement directTransport = Assert.Single(
unscouted.Descendants("PackageReference"),
static item => (string?)item.Attribute("Include") == "LiteNetLib");
Assert.Equal("[2.1.4]", (string?)directTransport.Attribute("Version"));
using JsonDocument consumers = JsonDocument.Parse(
File.ReadAllText(Path.Combine(root, "eng/consumer-revisions.json")));
JsonElement[] pinnedConsumers = consumers.RootElement.GetProperty("consumers")
.EnumerateArray()
.ToArray();
Assert.Equal(
["SpaceGame", "Unscouted"],
pinnedConsumers.Select(static item => item.GetProperty("name").GetString()!).ToArray());
Assert.All(pinnedConsumers, static item =>
Assert.Matches("^[0-9a-f]{40}$", item.GetProperty("revision").GetString()));
string realConsumerGate = File.ReadAllText(Path.Combine(root, "scripts", "verify-real-consumers.sh"));
Assert.Contains("<PackageReference Remove=\"FinalFactory.Rendezvous.Client\" />", realConsumerGate, StringComparison.Ordinal);
Assert.Contains("<PackageReference Remove=\"FinalFactory.Rendezvous.Contracts\" />", realConsumerGate, StringComparison.Ordinal);
}
[Fact]
public void AutomationProvidesNet8RuntimeForProcessVerification()
{
string root = FindRepositoryRoot();
string qualityWorkflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/ci.yml"));
Assert.Equal(2, qualityWorkflow.Split("8.0.128", StringSplitOptions.None).Length - 1);
string releaseWorkflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/release.yml"));
Assert.Contains("8.0.128", releaseWorkflow, StringComparison.Ordinal);
string releaseBuilder = File.ReadAllText(Path.Combine(root, "eng/release-builder.Dockerfile"));
Assert.Contains("runtime:8.0.28-noble@sha256:", releaseBuilder, StringComparison.Ordinal);
Assert.Contains("Microsoft.NETCore.App/8.0.28", releaseBuilder, StringComparison.Ordinal);
}
[Fact]
public void AutomationUsesPerRunContainerPorts()
{
string root = FindRepositoryRoot();
string compose = File.ReadAllText(Path.Combine(root, "deploy/compose/compose.yaml"));
Assert.Contains("${RENDEZVOUS_HTTP_HOST_PORT:-8080}:8080/tcp", compose, StringComparison.Ordinal);
Assert.Contains("${RENDEZVOUS_UDP_HOST_PORT:-9050}:9050/udp", compose, StringComparison.Ordinal);
Assert.Contains("${RENDEZVOUS_CONFIG_SOURCE:-./appsettings.Production.json}", compose, StringComparison.Ordinal);
Assert.Contains("${RENDEZVOUS_SECRET_SOURCE:-./secrets/signing-key}", compose, StringComparison.Ordinal);
foreach (string workflowName in new[] { "ci.yml", "release.yml" })
{
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows", workflowName));
Assert.Contains("GITHUB_RUN_ID", workflow, StringComparison.Ordinal);
Assert.Contains("runner_network", workflow, StringComparison.Ordinal);
Assert.Contains("docker network connect", workflow, StringComparison.Ordinal);
Assert.Contains("service_ip", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_HTTP_HOST_PORT", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_UDP_HOST_PORT", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_SMOKE_HTTP_URL", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_SMOKE_UDP_ENDPOINT", workflow, StringComparison.Ordinal);
Assert.Contains("runner_workspace_source", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_CONFIG_SOURCE", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_SECRET_SOURCE", workflow, StringComparison.Ordinal);
}
}
[Fact]
public void ContainerGateUsesValidInspectTemplates()
{
string root = FindRepositoryRoot();
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/ci.yml"));
Assert.Contains(
"eq .Destination \"/app/appsettings.Production.json\"",
workflow,
StringComparison.Ordinal);
Assert.Contains(
"eq .Destination \"/run/secrets/rendezvous-signing-key\"",
workflow,
StringComparison.Ordinal);
Assert.DoesNotContain("eq .Destination \\\"", workflow, StringComparison.Ordinal);
}
[Fact]
public void ServerSourceManifestIsCompleteSortedAndDeterministic()
{
string root = FindRepositoryRoot();
string projectDirectory = Path.Combine(root, "src/FinalFactory.Rendezvous.Server");
XDocument project = XDocument.Load(Path.Combine(projectDirectory, "FinalFactory.Rendezvous.Server.csproj"));
string[] declared = project.Descendants("Compile")
.Select(static item => (string)item.Attribute("Include")!)
.ToArray();
string[] actual = Directory.GetFiles(projectDirectory, "*.cs", SearchOption.AllDirectories)
.Where(static path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.Select(path => Path.GetRelativePath(projectDirectory, path).Replace(Path.DirectorySeparatorChar, '/'))
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(actual, declared);
}
[Fact]
public void ProductionReadinessRecordIsFailClosedAndCanaryEvidenceIsRedacted()
{
string root = FindRepositoryRoot();
using JsonDocument readiness = JsonDocument.Parse(File.ReadAllText(Path.Combine(
root,
"docs/evidence/production-readiness-v1.json")));
JsonElement document = readiness.RootElement;
Assert.Equal(1, document.GetProperty("schemaVersion").GetInt32());
Assert.Equal("rendezvous-production-readiness", document.GetProperty("kind").GetString());
Assert.Matches("^[0-9a-f]{40}$", document.GetProperty("evaluatedCommit").GetString());
JsonElement[] local = document.GetProperty("localGates").EnumerateArray().ToArray();
JsonElement[] external = document.GetProperty("externalGates").EnumerateArray().ToArray();
Assert.Equal(6, local.Length);
Assert.Equal(13, external.Length);
JsonElement[] gates = local.Concat(external).ToArray();
Assert.Equal(gates.Length, gates.Select(static gate => gate.GetProperty("id").GetString()).Distinct().Count());
Assert.All(gates, static gate =>
{
Assert.True(gate.GetProperty("status").GetString() is "pass" or "pending" or "fail");
string evidence = Assert.IsType<string>(gate.GetProperty("evidenceRef").GetString());
Assert.False(Path.IsPathRooted(evidence));
Assert.DoesNotContain("..", evidence, StringComparison.Ordinal);
Assert.True(gate.GetProperty("note").GetString()!.Length <= 240);
});
bool allPass = gates.All(static gate => gate.GetProperty("status").GetString() == "pass");
Assert.Equal(allPass ? "ready" : "not-ready", document.GetProperty("decision").GetString());
string canary = File.ReadAllText(Path.Combine(root, "scripts/run-real-network-canary.sh"));
Assert.Contains("umask 077", canary, StringComparison.Ordinal);
Assert.Contains("client-expected-failure", canary, StringComparison.Ordinal);
Assert.Contains("exit_code\" -eq 12", canary, StringComparison.Ordinal);
Assert.Contains(".addressFamily == $family", canary, StringComparison.Ordinal);
Assert.Contains("identifiers:\"not-in-summary\"", canary, StringComparison.Ordinal);
Assert.DoesNotContain("jq -c . \"$raw_log\"", canary, StringComparison.Ordinal);
string checker = File.ReadAllText(Path.Combine(root, "eng/check_production_readiness.py"));
Assert.Contains("return 3", checker, StringComparison.Ordinal);
Assert.Contains("FORBIDDEN_KEY_PARTS", checker, StringComparison.Ordinal);
Assert.Contains("decision must be", checker, StringComparison.Ordinal);
Assert.Contains("candidate capacity evidence", checker, StringComparison.Ordinal);
Assert.Contains("rendezvous-external-gate-attestation", checker, StringComparison.Ordinal);
Assert.Contains("does not resolve to a repository evidence file", checker, StringComparison.Ordinal);
}
private static string Property(XDocument document, string name) =>
document.Descendants(name).Single().Value;
private static string FindRepositoryRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "Rendezvous.slnx")))
{
return directory.FullName;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
}
}