Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Contracts/ContractSerializationTests.cs
KyuubiYoru 49564c7e7e
quality-gate / quality (push) Successful in 55s
feat: add presence-gated session leases (#7)
Closes #7
2026-07-16 05:58:47 +02:00

106 lines
3.9 KiB
C#

using System.Text.Json;
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class ContractSerializationTests
{
public static TheoryData<string, Type> GoldenJsonVectors => new()
{
{ "register-session.json", typeof(RegisterSessionRequest) },
{ "register-session-response.json", typeof(RegisterSessionResponse) },
{ "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<SessionListingId>(
"\"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<CreateJoinAttemptRequest>(
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<JsonException>(() => JsonSerializer.Deserialize<CreateJoinAttemptRequest>(
missingVersion,
ContractJson.Options));
}
[Fact]
public void UnknownEnumNamesAndNumericValuesAreRejected()
{
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ReportConnectionOutcomeRequest>(
"{\"contractVersion\":1,\"outcome\":\"futureOutcome\",\"elapsedMilliseconds\":1}",
ContractJson.Options));
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ReportConnectionOutcomeRequest>(
"{\"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<InvalidOperationException>(() =>
ContractJson.Options.WriteIndented = true);
}
}