using System.Text.Json; namespace FinalFactory.Rendezvous.Tests.Contracts; public sealed class OpenApiCompatibilityTests { private static readonly string[] ExpectedPaths = [ "/health/live", "/health/ready", "/v1/join-attempts", "/v1/join-attempts/{attemptId}", "/v1/join-attempts/{attemptId}/outcome", "/v1/operator/drain", "/v1/operator/keys/revoke", "/v1/operator/listings/revoke", "/v1/operator/principals/revoke", "/v1/operator/status", "/v1/sessions", "/v1/sessions/{listingId}", "/v1/sessions/{listingId}/join-attempts", "/v1/sessions/{listingId}/renew", ]; private static readonly string[] ExpectedListingProperties = [ "buildVersion", "capacity", "contractVersion", "dedicatedFallback", "displayName", "environmentId", "gameId", "listingId", "metadata", "protocolVersion", "publisherTrustMode", "regionId", "visibility", ]; [Fact] public void GeneratedOpenApiContainsTheFrozenV1Surface() { string path = Path.Combine( ContractTestFiles.Directory, "../../../../../docs/api/rendezvous-v1.json"); using JsonDocument document = JsonDocument.Parse(File.ReadAllText(Path.GetFullPath(path))); JsonElement root = document.RootElement; Assert.Equal("3.1.1", root.GetProperty("openapi").GetString()); string[] paths = root.GetProperty("paths") .EnumerateObject() .Select(static item => item.Name) .Order(StringComparer.Ordinal) .ToArray(); Assert.Equal(ExpectedPaths, paths); JsonElement schemas = root.GetProperty("components").GetProperty("schemas"); Assert.Equal("string", schemas.GetProperty("GameId").GetProperty("type").GetString()); Assert.Equal("uuid", schemas.GetProperty("SessionListingId").GetProperty("format").GetString()); string[] listingProperties = schemas.GetProperty("SessionListing") .GetProperty("properties") .EnumerateObject() .Select(static item => item.Name) .Order(StringComparer.Ordinal) .ToArray(); Assert.Equal(ExpectedListingProperties, listingProperties); Assert.DoesNotContain(listingProperties, static property => property.Contains("token", StringComparison.OrdinalIgnoreCase) || property.Contains("playerId", StringComparison.OrdinalIgnoreCase)); JsonElement dedicatedFallback = schemas.GetProperty("SessionListing") .GetProperty("properties") .GetProperty("dedicatedFallback"); JsonElement fallbackReference = Assert.Single( dedicatedFallback.GetProperty("oneOf").EnumerateArray(), static schema => schema.TryGetProperty("$ref", out _)); Assert.Equal( "#/components/schemas/NetworkEndpoint", fallbackReference.GetProperty("$ref").GetString()); JsonElement outcomeReportProperties = schemas.GetProperty("ReportConnectionOutcomeRequest") .GetProperty("properties"); Assert.True(outcomeReportProperties.TryGetProperty("elapsedBucket", out _)); Assert.True(outcomeReportProperties.TryGetProperty("elapsedMilliseconds", out _)); Assert.True(outcomeReportProperties.TryGetProperty("diagnosticCode", out _)); string[] outcomeNames = schemas.GetProperty("ConnectionOutcomeKind") .GetProperty("enum") .EnumerateArray() .Select(static value => value.GetString()!) .ToArray(); Assert.Contains("timedOut", outcomeNames); Assert.Contains("staleHost", outcomeNames); Assert.Contains("transportFailed", outcomeNames); Assert.Contains("punchTimedOut", outcomeNames); Assert.Contains("directConnectTimedOut", outcomeNames); Assert.Contains("transportError", outcomeNames); JsonElement publisherBearer = root.GetProperty("components") .GetProperty("securitySchemes") .GetProperty("PublisherBearer"); Assert.Equal("http", publisherBearer.GetProperty("type").GetString()); Assert.Equal("bearer", publisherBearer.GetProperty("scheme").GetString()); JsonElement attemptCapability = root.GetProperty("components") .GetProperty("securitySchemes") .GetProperty("JoinAttemptCapability"); Assert.Equal("apiKey", attemptCapability.GetProperty("type").GetString()); Assert.Equal( "X-Rendezvous-Client-Punch-Capability", attemptCapability.GetProperty("name").GetString()); JsonElement operatorBearer = root.GetProperty("components") .GetProperty("securitySchemes") .GetProperty("OperatorBearer"); Assert.Equal("http", operatorBearer.GetProperty("type").GetString()); Assert.Equal("bearer", operatorBearer.GetProperty("scheme").GetString()); (string Path, string Method)[] publisherOperations = [ ("/v1/sessions", "post"), ("/v1/sessions/{listingId}", "put"), ("/v1/sessions/{listingId}", "delete"), ("/v1/sessions/{listingId}/renew", "post"), ]; foreach ((string operationPath, string method) in publisherOperations) { JsonElement security = root.GetProperty("paths") .GetProperty(operationPath) .GetProperty(method) .GetProperty("security"); Assert.True(security[0].TryGetProperty("PublisherBearer", out _)); } (string Path, string Method)[] operatorOperations = [ ("/v1/operator/status", "get"), ("/v1/operator/listings/revoke", "post"), ("/v1/operator/principals/revoke", "post"), ("/v1/operator/keys/revoke", "post"), ("/v1/operator/drain", "post"), ]; foreach ((string operationPath, string method) in operatorOperations) { JsonElement security = root.GetProperty("paths") .GetProperty(operationPath) .GetProperty(method) .GetProperty("security"); Assert.True(security[0].TryGetProperty("OperatorBearer", out _)); } JsonElement cancelParameters = root.GetProperty("paths") .GetProperty("/v1/join-attempts/{attemptId}") .GetProperty("delete") .GetProperty("parameters"); JsonElement cancelCapability = Assert.Single(cancelParameters.EnumerateArray(), static parameter => parameter.GetProperty("in").GetString() == "header" && parameter.GetProperty("name").GetString() == "X-Rendezvous-Client-Punch-Capability"); Assert.True(cancelCapability.GetProperty("required").GetBoolean()); foreach ((string operationPath, string method) in new[] { ("/v1/join-attempts/{attemptId}", "delete"), ("/v1/join-attempts/{attemptId}/outcome", "post"), }) { JsonElement security = root.GetProperty("paths") .GetProperty(operationPath) .GetProperty(method) .GetProperty("security"); Assert.True(security[0].TryGetProperty("JoinAttemptCapability", out _)); } JsonElement hostPollParameters = root.GetProperty("paths") .GetProperty("/v1/sessions/{listingId}/join-attempts") .GetProperty("get") .GetProperty("parameters"); JsonElement leaseToken = Assert.Single(hostPollParameters.EnumerateArray(), static parameter => parameter.GetProperty("in").GetString() == "header" && parameter.GetProperty("name").GetString() == "X-Rendezvous-Lease-Token"); Assert.True(leaseToken.GetProperty("required").GetBoolean()); int overloadContracts = 0; foreach (JsonProperty pathItem in root.GetProperty("paths").EnumerateObject()) { foreach (JsonProperty operation in pathItem.Value.EnumerateObject().Where( static item => item.Name is "get" or "post" or "put" or "delete")) { JsonElement responses = operation.Value.GetProperty("responses"); foreach (JsonProperty response in responses.EnumerateObject()) { JsonElement correlation = response.Value.GetProperty("headers") .GetProperty("X-Rendezvous-Correlation-ID"); Assert.Equal( "string", correlation.GetProperty("schema").GetProperty("type").GetString()); } if (!responses.TryGetProperty("429", out JsonElement overloaded)) { continue; } overloadContracts++; JsonElement retryAfter = overloaded.GetProperty("headers") .GetProperty("Retry-After"); Assert.Equal( "integer", retryAfter.GetProperty("schema").GetProperty("type").GetString()); } } Assert.Equal(17, overloadContracts); (string Path, string Method)[] bodyOperations = [ ("/v1/sessions", "post"), ("/v1/sessions/{listingId}/renew", "post"), ("/v1/sessions/{listingId}", "put"), ("/v1/sessions/{listingId}", "delete"), ("/v1/join-attempts", "post"), ("/v1/join-attempts/{attemptId}/outcome", "post"), ("/v1/operator/listings/revoke", "post"), ("/v1/operator/principals/revoke", "post"), ("/v1/operator/keys/revoke", "post"), ("/v1/operator/drain", "post"), ]; foreach ((string operationPath, string method) in bodyOperations) { Assert.True(root.GetProperty("paths") .GetProperty(operationPath) .GetProperty(method) .GetProperty("responses") .TryGetProperty("413", out _)); } } }