|
| 1 | +using System; |
| 2 | +using System.Linq; |
| 3 | +using System.Net; |
| 4 | +using System.Text; |
| 5 | +using ManagedCode.Communication.Orleans.Converters; |
| 6 | +using ManagedCode.Communication.Orleans.Extensions; |
| 7 | +using ManagedCode.Communication.Orleans.Surrogates; |
| 8 | +using ManagedCode.Communication.Tests.Orleans.Fixtures; |
| 9 | +using Microsoft.Extensions.DependencyInjection; |
| 10 | +using Microsoft.Extensions.Options; |
| 11 | +using Newtonsoft.Json; |
| 12 | +using Orleans.Serialization; |
| 13 | +using Orleans.Storage; |
| 14 | +using Orleans.TestingHost; |
| 15 | +using Shouldly; |
| 16 | + |
| 17 | +namespace ManagedCode.Communication.Tests.Orleans; |
| 18 | + |
| 19 | +[ClassDataSource<OrleansClusterFixture>(Shared = SharedType.PerClass)] |
| 20 | +[NotInParallel(nameof(OrleansJsonStorageSerializationTests))] |
| 21 | +public sealed class OrleansJsonStorageSerializationTests(OrleansClusterFixture fixture) |
| 22 | +{ |
| 23 | + private const string FailureTitle = "native-storage-failure"; |
| 24 | + private const string FailureDetail = "Preserve native stored failure"; |
| 25 | + private const string ExtensionKey = "owner"; |
| 26 | + private const string ExtensionValue = "creator"; |
| 27 | + private const string LegacySuccess = "{\"IsSuccess\":true,\"Value\":42,\"Problem\":null}"; |
| 28 | + private const string MissingFlag = "{\"Value\":42}"; |
| 29 | + private const string ConflictingFlags = "{\"IsSuccess\":true,\"IsFailed\":true,\"Value\":42}"; |
| 30 | + private const string InvalidFlag = "{\"IsSuccess\":\"true\",\"Value\":42}"; |
| 31 | + |
| 32 | + private JsonGrainStorageSerializer Serializer() => new( |
| 33 | + ((InProcessSiloHandle)fixture.Cluster.Primary!).ServiceProvider.GetRequiredService<OrleansJsonSerializer>()); |
| 34 | + |
| 35 | + [Test] |
| 36 | + [Arguments(0)] |
| 37 | + [Arguments(42)] |
| 38 | + public void JsonStorageMustPreserveSuccessfulResultFlagAndValue(int value) |
| 39 | + { |
| 40 | + var serializer = Serializer(); |
| 41 | + var expected = Result<int>.Succeed(value); |
| 42 | + var restored = serializer.Deserialize<Result<int>>(serializer.Serialize(expected)); |
| 43 | + restored.IsSuccess.ShouldBeTrue(); |
| 44 | + restored.Value.ShouldBe(expected.Value); |
| 45 | + } |
| 46 | + |
| 47 | + [Test] |
| 48 | + public void JsonStorageMustPreserveFailedProblemValueAndNestedResults() |
| 49 | + { |
| 50 | + var problem = Problem.Create(FailureTitle, FailureDetail, (int)HttpStatusCode.Conflict); |
| 51 | + problem.Extensions[ExtensionKey] = ExtensionValue; |
| 52 | + var expected = new ResultTSurrogateConverter<int>().ConvertFromSurrogate(new ResultTSurrogate<int>(false, 42, problem)); |
| 53 | + var serializer = Serializer(); |
| 54 | + var restored = serializer.Deserialize<StoredState>(serializer.Serialize(new StoredState { Update = expected })); |
| 55 | + restored!.Update.IsFailed.ShouldBeTrue(); |
| 56 | + restored.Update.Value.ShouldBe(42); |
| 57 | + restored.Update.Problem!.Title.ShouldBe(FailureTitle); |
| 58 | + restored.Update.Problem.Detail.ShouldBe(FailureDetail); |
| 59 | + restored.Update.Problem.StatusCode.ShouldBe(problem.StatusCode); |
| 60 | + restored.Update.Problem.Extensions[ExtensionKey]?.ToString().ShouldBe(ExtensionValue); |
| 61 | + } |
| 62 | + |
| 63 | + [Test] |
| 64 | + public void JsonStorageMustPreserveUntypedAndDefaultValuedResults() |
| 65 | + { |
| 66 | + var serializer = Serializer(); |
| 67 | + serializer.Deserialize<Result>(serializer.Serialize(Result.Succeed())).IsSuccess.ShouldBeTrue(); |
| 68 | + serializer.Deserialize<Result>(serializer.Serialize(Result.Fail(Problem.Create(FailureTitle, FailureDetail, 409)))).IsFailed.ShouldBeTrue(); |
| 69 | + serializer.Deserialize<Result<bool>>(serializer.Serialize(Result<bool>.Succeed(false))).IsSuccess.ShouldBeTrue(); |
| 70 | + } |
| 71 | + |
| 72 | + [Test] |
| 73 | + public void NativeLegacyPascalCaseSuccessMustRemainSuccessful() |
| 74 | + { |
| 75 | + var restored = Serializer().Deserialize<Result<int>>(new BinaryData(Encoding.UTF8.GetBytes(LegacySuccess))); |
| 76 | + restored.IsSuccess.ShouldBeTrue(); |
| 77 | + restored.Value.ShouldBe(42); |
| 78 | + } |
| 79 | + |
| 80 | + [Test] |
| 81 | + [Arguments(0)] |
| 82 | + [Arguments(42)] |
| 83 | + public void NativeLegacyFailedProblemMustPreserveStatusAndExtensions(int value) |
| 84 | + { |
| 85 | + var problem = Problem.Create(FailureTitle, FailureDetail, (int)HttpStatusCode.Conflict); |
| 86 | + problem.Extensions[ExtensionKey] = ExtensionValue; |
| 87 | + var legacySerializer = new JsonGrainStorageSerializer(new OrleansJsonSerializer( |
| 88 | + Options.Create(new OrleansJsonSerializerOptions()))); |
| 89 | + var expected = new ResultTSurrogateConverter<int>().ConvertFromSurrogate(new ResultTSurrogate<int>(false, value, problem)); |
| 90 | + var data = legacySerializer.Serialize(expected); |
| 91 | + var restored = Serializer().Deserialize<Result<int>>(data); |
| 92 | + restored.IsFailed.ShouldBeTrue(); |
| 93 | + restored.Value.ShouldBe(value); |
| 94 | + restored.Problem.Title.ShouldBe(FailureTitle); |
| 95 | + restored.Problem.Detail.ShouldBe(FailureDetail); |
| 96 | + restored.Problem.StatusCode.ShouldBe(problem.StatusCode); |
| 97 | + restored.Problem.Extensions[ExtensionKey]?.ToString().ShouldBe(ExtensionValue); |
| 98 | + } |
| 99 | + |
| 100 | + [Test] |
| 101 | + [Arguments(MissingFlag)] |
| 102 | + [Arguments(InvalidFlag)] |
| 103 | + [Arguments(ConflictingFlags)] |
| 104 | + public void InvalidStoredFlagsMustFailClosed(string content) => |
| 105 | + Should.Throw<JsonSerializationException>(() => Serializer().Deserialize<Result<int>>(new BinaryData(Encoding.UTF8.GetBytes(content)))); |
| 106 | + |
| 107 | + [Test] |
| 108 | + public void StandaloneRegistrationMustBeIdempotentAndPreserveNativeJson() |
| 109 | + { |
| 110 | + using var services = new ServiceCollection().AddCommunicationOrleansJsonStorage().AddCommunicationOrleansJsonStorage().BuildServiceProvider(); |
| 111 | + var settings = services.GetRequiredService<IOptions<OrleansJsonSerializerOptions>>().Value; |
| 112 | + settings.JsonSerializerSettings.Converters.OfType<CommunicationResultJsonConverter>().Count().ShouldBe(1); |
| 113 | + var serializer = new JsonGrainStorageSerializer(new OrleansJsonSerializer(Options.Create(settings))); |
| 114 | + serializer.Deserialize<Result<int>>(serializer.Serialize(Result<int>.Succeed(42))).IsSuccess.ShouldBeTrue(); |
| 115 | + } |
| 116 | + |
| 117 | + [global::Orleans.GenerateSerializer] |
| 118 | + internal sealed class StoredState |
| 119 | + { |
| 120 | + [global::Orleans.Id(0)] |
| 121 | + public Result<int> Update { get; init; } |
| 122 | + } |
| 123 | +} |
0 commit comments