using System.Text.Json; using FinalFactory.Rendezvous.Contracts; namespace FinalFactory.Rendezvous.Tests.Contracts; public sealed class ContractSerializationTests { public static TheoryData GoldenJsonVectors => new() { { "register-session.json", typeof(RegisterSessionRequest) }, { "browse-sessions.json", typeof(BrowseSessionsResponse) }, { "create-join-response.json", typeof(CreateJoinAttemptResponse) }, { "api-error.json", typeof(ApiError) }, }; [Theory] [MemberData(nameof(GoldenJsonVectors))] public void CanonicalJsonRoundtripsGoldenVectors(string fileName, Type contractType) { string expected = ContractTestFiles.Read(fileName); object? value = JsonSerializer.Deserialize(expected, contractType, ContractJson.Options); Assert.NotNull(value); Assert.Equal(expected, JsonSerializer.Serialize(value, contractType, ContractJson.Options)); } [Fact] public void IdentifiersAreSerializedAsStrings() { SessionListingId id = new(new Guid("00112233-4455-6677-8899-aabbccddeeff")); Assert.Equal( "\"00112233-4455-6677-8899-aabbccddeeff\"", JsonSerializer.Serialize(id, ContractJson.Options)); Assert.Equal(id, JsonSerializer.Deserialize( "\"00112233-4455-6677-8899-aabbccddeeff\"", ContractJson.Options)); } [Fact] public void UnknownObjectFieldsAreIgnoredForAdditiveV1Changes() { const string json = """ {"contractVersion":1,"idempotencyKey":"join-001","gameId":"space-game","environmentId":"production","listingId":"00112233-4455-6677-8899-aabbccddeeff","protocolVersion":7,"futureField":{"nested":true}} """; CreateJoinAttemptRequest? request = JsonSerializer.Deserialize( json, ContractJson.Options); Assert.NotNull(request); Assert.Equal(new GameId("space-game"), request.GameId); Assert.Equal(7u, request.ProtocolVersion); } [Fact] public void MissingNormativeFieldsAreRejectedInsteadOfDefaulted() { const string missingVersion = """ {"idempotencyKey":"join-001","gameId":"space-game","environmentId":"production","listingId":"00112233-4455-6677-8899-aabbccddeeff","protocolVersion":7} """; Assert.Throws(() => JsonSerializer.Deserialize( missingVersion, ContractJson.Options)); } [Fact] public void UnknownEnumNamesAndNumericValuesAreRejected() { Assert.Throws(() => JsonSerializer.Deserialize( "{\"contractVersion\":1,\"outcome\":\"futureOutcome\",\"elapsedMilliseconds\":1}", ContractJson.Options)); Assert.Throws(() => JsonSerializer.Deserialize( "{\"contractVersion\":1,\"outcome\":99,\"elapsedMilliseconds\":1}", ContractJson.Options)); } [Theory] [InlineData(0)] [InlineData(2)] [InlineData(int.MaxValue)] public void UnknownContractVersionsFailPredictably(int version) { Assert.Equal( RendezvousErrorCode.UnsupportedContractVersion, ContractValidation.ValidateContractVersion(version)); } [Fact] public void GameplayProtocolCompatibilityIsExactAndBuildIndependent() { Assert.True(ContractValidation.AreProtocolsCompatible(7, 7)); Assert.False(ContractValidation.AreProtocolsCompatible(7, 8)); } [Fact] public void SharedCanonicalOptionsCannotBeMutatedByConsumers() { Assert.True(ContractJson.Options.IsReadOnly); Assert.Throws(() => ContractJson.Options.WriteIndented = true); } }