Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs
T
KyuubiYoru 7fb85059fb
quality-gate / quality (push) Failing after 1m28s
quality-gate / container (push) Has been skipped
docs: add integration guides and incident runbooks (#20)
2026-07-16 18:25:10 +02:00

214 lines
10 KiB
C#

using System.Text.Json;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace FinalFactory.Rendezvous.Tests.Documentation;
public sealed partial class DocumentationContractTests
{
private static readonly string[] IncidentScenarios =
[
"Abuse or authentication spike",
"Signing key or issuer compromise",
"Targeted listing or publisher revocation",
"Planned restart or crash recovery",
"Release rollback",
"Capacity saturation",
"Privacy or telemetry incident",
"Dependency or base-image upgrade",
];
[Fact]
public void TestClientGuideDocumentsTheExecutableSuccessAndFailureContracts()
{
string root = FindRepositoryRoot();
string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "test-client.md"));
Assert.Contains("space-game --environment smoke --region local --protocol 1", guide, StringComparison.Ordinal);
Assert.Contains("mint-local-publisher-credential.sh", guide, StringComparison.Ordinal);
Assert.Contains("join.connected", guide, StringComparison.Ordinal);
Assert.Contains("join.direct-traffic", guide, StringComparison.Ordinal);
Assert.Contains("browse.completed", guide, StringComparison.Ordinal);
Assert.Contains("--script --json", guide, StringComparison.Ordinal);
Assert.Contains("--run-seconds 60", guide, StringComparison.Ordinal);
Assert.Contains("for attempt in {1..45}", guide, StringComparison.Ordinal);
Assert.Contains("before the terminal-3", guide, StringComparison.Ordinal);
Assert.Contains("test \"$status\" -eq 11", guide, StringComparison.Ordinal);
Assert.Contains("no relay", guide, StringComparison.OrdinalIgnoreCase);
Assert.Contains("cannot guarantee", guide, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotMatch(ReusableCredential(), guide);
}
[Fact]
public void LocalCredentialHelperIsFixedScopeAndSmokeDelegatesToIt()
{
string root = FindRepositoryRoot();
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
Assert.Contains("\"gameId\": \"space-game\"", helper, StringComparison.Ordinal);
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
Assert.Contains("now + 600", helper, StringComparison.Ordinal);
Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal);
Assert.Contains("parent.st_mode & 0o077", helper, StringComparison.Ordinal);
Assert.Contains("metadata.st_mode & 0o022", helper, StringComparison.Ordinal);
Assert.Contains("metadata.st_nlink != 1", helper, StringComparison.Ordinal);
Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal);
Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal);
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
}
[Fact]
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
{
string root = FindRepositoryRoot();
string runbooks = File.ReadAllText(Path.Combine(root, "docs", "operations", "incident-runbooks.md"));
for (int index = 0; index < IncidentScenarios.Length; index++)
{
string heading = $"## {IncidentScenarios[index]}";
int start = runbooks.IndexOf(heading, StringComparison.Ordinal);
Assert.True(start >= 0, $"Missing incident runbook heading: {heading}");
int end = index + 1 < IncidentScenarios.Length
? runbooks.IndexOf($"## {IncidentScenarios[index + 1]}", start, StringComparison.Ordinal)
: runbooks.Length;
Assert.True(end > start, $"Could not find the end of runbook: {heading}");
string scenario = runbooks[start..end];
Assert.Contains("### Detect", scenario, StringComparison.Ordinal);
Assert.Contains("### Contain", scenario, StringComparison.Ordinal);
Assert.Contains("### Recover", scenario, StringComparison.Ordinal);
Assert.Contains("### Verify", scenario, StringComparison.Ordinal);
}
Assert.DoesNotMatch(ReusableCredential(), runbooks);
}
[Fact]
public void DocumentedOperatorOperationsAndBodiesMatchTheReleasedOpenApi()
{
string root = FindRepositoryRoot();
string runbooks = File.ReadAllText(Path.Combine(root, "docs", "operations", "incident-runbooks.md"));
using JsonDocument openApi = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root, "docs", "api", "rendezvous-v1.json")));
JsonElement paths = openApi.RootElement.GetProperty("paths");
(string Method, string Path)[] documentedOperations = OperatorRoute().Matches(runbooks)
.Cast<Match>()
.Select(static match => (
match.Groups["method"].Value.ToLowerInvariant(),
match.Groups["path"].Value))
.Distinct()
.ToArray();
Assert.NotEmpty(documentedOperations);
Assert.All(documentedOperations, operation =>
Assert.True(
paths.TryGetProperty(operation.Path, out JsonElement path)
&& path.TryGetProperty(operation.Method, out _),
$"OpenAPI does not contain {operation.Method.ToUpperInvariant()} {operation.Path}."));
Match[] actions = OperatorAction().Matches(runbooks).Cast<Match>().ToArray();
Assert.Equal(4, actions.Length);
foreach (Match action in actions)
{
string method = action.Groups["method"].Value.ToLowerInvariant();
string path = action.Groups["path"].Value;
using JsonDocument body = JsonDocument.Parse(action.Groups["body"].Value);
string reference = paths.GetProperty(path)
.GetProperty(method)
.GetProperty("requestBody")
.GetProperty("content")
.GetProperty("application/json")
.GetProperty("schema")
.GetProperty("$ref")
.GetString()!;
string schemaName = reference["#/components/schemas/".Length..];
string[] required = openApi.RootElement.GetProperty("components")
.GetProperty("schemas")
.GetProperty(schemaName)
.GetProperty("required")
.EnumerateArray()
.Select(static property => property.GetString()!)
.Order(StringComparer.Ordinal)
.ToArray();
string[] documented = body.RootElement.EnumerateObject()
.Select(static property => property.Name)
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(required, documented);
}
}
[Fact]
public void SdkGuideMatchesTheReleasedPackageAndTransportMatrix()
{
string root = FindRepositoryRoot();
string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "sdk-seams.md"));
using JsonDocument compatibility = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root, "docs", "releases", "compatibility.json")));
JsonElement matrix = compatibility.RootElement;
JsonElement packages = matrix.GetProperty("packages");
string clientVersion = packages.GetProperty("FinalFactory.Rendezvous.Client").GetString()!;
string contractsVersion = packages.GetProperty("FinalFactory.Rendezvous.Contracts").GetString()!;
string liteNetLibVersion = matrix.GetProperty("transport").GetProperty("version").GetString()!;
string clientFramework = XDocument.Load(Path.Combine(
root,
"src",
"FinalFactory.Rendezvous.Client",
"FinalFactory.Rendezvous.Client.csproj"))
.Descendants("TargetFramework")
.Single()
.Value;
int httpVersion = matrix.GetProperty("contracts").GetProperty("http")[0].GetInt32();
int udpVersion = matrix.GetProperty("contracts").GetProperty("udp")[0].GetInt32();
int ticketVersion = matrix.GetProperty("contracts").GetProperty("connectionTicket")[0].GetInt32();
Assert.Contains(
$"FinalFactory.Rendezvous.Client\" Version=\"{clientVersion}\"",
guide,
StringComparison.Ordinal);
Assert.Contains(
$"FinalFactory.Rendezvous.Contracts\" Version=\"{contractsVersion}\"",
guide,
StringComparison.Ordinal);
Assert.Contains($"targets `{clientFramework}`", guide, StringComparison.Ordinal);
Assert.Contains($"LiteNetLib `{liteNetLibVersion}`", guide, StringComparison.Ordinal);
Assert.Contains(
"https://git.finalfactory.de/api/packages/HeiKyu/nuget/index.json",
guide,
StringComparison.Ordinal);
Assert.Equal(httpVersion, udpVersion);
Assert.Equal(httpVersion, ticketVersion);
Assert.Contains($"contract version `{httpVersion}`", guide, StringComparison.Ordinal);
}
[GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)]
private static partial Regex ReusableCredential();
[GeneratedRegex(@"(?<method>GET|POST) `?(?<path>/v1/operator/[a-z/-]+)`?", RegexOptions.CultureInvariant)]
private static partial Regex OperatorRoute();
[GeneratedRegex(@"\| `(?<method>POST) (?<path>/v1/operator/[a-z/-]+)` \| `(?<body>\{[^`]+\})` \|", RegexOptions.CultureInvariant)]
private static partial Regex OperatorAction();
private static string FindRepositoryRoot()
{
DirectoryInfo? current = new(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "Rendezvous.slnx")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Could not locate the repository root.");
}
}