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()!.InformationalVersion.Split('+')[0]); Assert.Equal(releaseVersion, typeof(ContractLimits).Assembly.GetCustomAttribute()!.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("", realConsumerGate, StringComparison.Ordinal); Assert.Contains("", 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 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(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."); } }