#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MessagePack; using NUnit.Framework; namespace GameCult.Networking.Tests { [TestFixture] public sealed class CultNetOperationServerTests { [Test] public async Task DispatchesTypedRequestAndCorrelatedReplyWithoutEnvelopePlumbing() { var transport = new FakeServer(); var peer = new FakePeer(); CultNetOperationContext? observed = null; using var operations = new CultNetOperationServer(transport, "sample.provider") .Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => { observed = context; return Task.FromResult(new AddReceipt { Count = context.Value.Amount + 4 }); }); await transport.DispatchAsync(Request(new AddRequest { Amount = 3 }), peer); var response = peer.Messages.OfType().Single(); var receipt = MessagePackSerializer.Deserialize( Convert.FromBase64String(response.Payload), CultNetSchemaMessageSerialization.Options); Assert.Multiple(() => { Assert.That(observed!.Value.Amount, Is.EqualTo(3)); Assert.That(observed.IdempotencyKey, Is.EqualTo("command-7")); Assert.That(observed.SourceRuntimeId, Is.EqualTo("sample.browser")); Assert.That(response.MessageId, Is.EqualTo("command-7")); Assert.That(response.ServiceId, Is.EqualTo("sample.counter")); Assert.That(response.Operation, Is.EqualTo("sample.counter.add")); Assert.That(response.Status, Is.EqualTo("accepted")); Assert.That(response.PayloadSchema, Is.EqualTo("sample.add_receipt.v1")); Assert.That(response.SourceRuntimeId, Is.EqualTo("sample.provider")); Assert.That(receipt.Count, Is.EqualTo(7)); }); } [Test] public async Task PreservesTypedRejectionAndDiagnostics() { var transport = new FakeServer(); var peer = new FakePeer(); using var operations = new CultNetOperationServer(transport) .Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", _ => Task.FromResult(CultNetOperationReply.Rejected( new AddReceipt { Count = 4 }, "counter is locked"))); await transport.DispatchAsync(Request(new AddRequest { Amount = 3 }), peer); var response = peer.Messages.OfType().Single(); Assert.That(response.Status, Is.EqualTo("rejected")); Assert.That(response.Diagnostics, Is.EqualTo(new[] { "counter is locked" })); Assert.That( MessagePackSerializer.Deserialize( Convert.FromBase64String(response.Payload), CultNetSchemaMessageSerialization.Options).Count, Is.EqualTo(4)); } [Test] public async Task RejectsRequestAddressedToAnotherRuntimeBeforeApplicationHandler() { var transport = new FakeServer(); var peer = new FakePeer(); var calls = 0; using var operations = new CultNetOperationServer(transport, "sample.provider") .Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => { calls++; return Task.FromResult(new AddReceipt { Count = context.Value.Amount }); }); var request = Request(new AddRequest { Amount = 3 }); request.TargetRuntimeId = "other.provider"; await transport.DispatchAsync(request, peer); Assert.That(calls, Is.Zero); var response = peer.Messages.OfType().Single(); var failure = MessagePackSerializer.Deserialize( Convert.FromBase64String(response.Payload), CultNetSchemaMessageSerialization.Options); Assert.Multiple(() => { Assert.That(response.Status, Is.EqualTo("denied")); Assert.That(failure.Code, Is.EqualTo("target-runtime-mismatch")); Assert.That(response.SourceRuntimeId, Is.EqualTo("sample.provider")); }); } [Test] public async Task RejectsWrongSchemaBeforeApplicationHandler() { var transport = new FakeServer(); var peer = new FakePeer(); var calls = 0; using var operations = new CultNetOperationServer(transport) .Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => { calls++; return Task.FromResult(new AddReceipt { Count = context.Value.Amount }); }); var request = Request(new AddRequest { Amount = 3 }); request.PayloadSchema = "wrong.v1"; await transport.DispatchAsync(request, peer); Assert.That(calls, Is.Zero); var response = peer.Messages.OfType().Single(); var failure = MessagePackSerializer.Deserialize( Convert.FromBase64String(response.Payload), CultNetSchemaMessageSerialization.Options); Assert.Multiple(() => { Assert.That(response.MessageId, Is.EqualTo("command-7")); Assert.That(response.Status, Is.EqualTo("invalid")); Assert.That(response.PayloadSchema, Is.EqualTo(CultNetOperationServer.FailureSchemaId)); Assert.That(failure.Code, Is.EqualTo("request-schema-mismatch")); Assert.That(failure.Message, Does.Contain("expected payload schema 'sample.add.v1'")); }); } [Test] public void RejectsDuplicateRouteAndDetachesOnDispose() { var transport = new FakeServer(); var operations = new CultNetOperationServer(transport); operations.Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => Task.FromResult(new AddReceipt { Count = context.Value.Amount })); Assert.That(() => operations.Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => Task.FromResult(new AddReceipt { Count = context.Value.Amount })), Throws.InvalidOperationException); Assert.That(transport.HasHandler(), Is.True); operations.Dispose(); Assert.That(transport.HasHandler(), Is.False); Assert.That(() => operations.Register( "sample.counter", "sample.counter.add", "sample.add.v1", "sample.add_receipt.v1", context => Task.FromResult(new AddReceipt { Count = context.Value.Amount })), Throws.TypeOf()); } private static CultNetOperationRequestMessage Request(AddRequest value) => new() { MessageId = "command-7", ServiceId = "sample.counter", Operation = "sample.counter.add", PayloadSchema = "sample.add.v1", PayloadEncoding = "messagepack-base64", Payload = Convert.ToBase64String(MessagePackSerializer.Serialize( value, CultNetSchemaMessageSerialization.Options)), SourceRuntimeId = "sample.browser", TargetRuntimeId = "sample.provider" }; [MessagePackObject] public sealed class AddRequest { [Key(0)] public int Amount { get; set; } } [MessagePackObject] public sealed class AddReceipt { [Key(0)] public int Count { get; set; } } private sealed class FakeServer : ICultNetSchemaServer { private readonly Dictionary _handlers = new(); public void OnCultNet(Func callback) where TMessage : ICultNetSchemaMessage => _handlers[typeof(TMessage)] = callback; public void RemoveCultNetMessageListener(Delegate callback) where TMessage : ICultNetSchemaMessage { if (_handlers.TryGetValue(typeof(TMessage), out var current) && current == callback) _handlers.Remove(typeof(TMessage)); } public bool HasHandler() where TMessage : ICultNetSchemaMessage => _handlers.ContainsKey(typeof(TMessage)); public Task DispatchAsync(TMessage message, ICultNetSchemaServerPeer peer) where TMessage : ICultNetSchemaMessage => ((Func)_handlers[typeof(TMessage)])(message, peer); } private sealed class FakePeer : ICultNetSchemaServerPeer { public List Messages { get; } = new(); public void SendCultNet(TMessage message) where TMessage : ICultNetSchemaMessage => Messages.Add(message); } } }