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())); } [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); } 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."); } }