214 lines
10 KiB
C#
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.");
|
|
}
|
|
}
|