CultLib/tests/GameCult.Networking.Tests/CultNetOperationServerTests.cs

241 lines
10 KiB
C#

#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<AddRequest>? observed = null;
using var operations = new CultNetOperationServer(transport, "sample.provider")
.Register<AddRequest, AddReceipt>(
"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<CultNetOperationResponseMessage>().Single();
var receipt = MessagePackSerializer.Deserialize<AddReceipt>(
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<AddRequest, AddReceipt>(
"sample.counter",
"sample.counter.add",
"sample.add.v1",
"sample.add_receipt.v1",
_ => Task.FromResult(CultNetOperationReply<AddReceipt>.Rejected(
new AddReceipt { Count = 4 },
"counter is locked")));
await transport.DispatchAsync(Request(new AddRequest { Amount = 3 }), peer);
var response = peer.Messages.OfType<CultNetOperationResponseMessage>().Single();
Assert.That(response.Status, Is.EqualTo("rejected"));
Assert.That(response.Diagnostics, Is.EqualTo(new[] { "counter is locked" }));
Assert.That(
MessagePackSerializer.Deserialize<AddReceipt>(
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<AddRequest, AddReceipt>(
"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<CultNetOperationResponseMessage>().Single();
var failure = MessagePackSerializer.Deserialize<CultNetOperationFailure>(
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<AddRequest, AddReceipt>(
"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<CultNetOperationResponseMessage>().Single();
var failure = MessagePackSerializer.Deserialize<CultNetOperationFailure>(
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<AddRequest, AddReceipt>(
"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<AddRequest, AddReceipt>(
"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<CultNetOperationRequestMessage>(), Is.True);
operations.Dispose();
Assert.That(transport.HasHandler<CultNetOperationRequestMessage>(), Is.False);
Assert.That(() => operations.Register<AddRequest, AddReceipt>(
"sample.counter",
"sample.counter.add",
"sample.add.v1",
"sample.add_receipt.v1",
context => Task.FromResult(new AddReceipt { Count = context.Value.Amount })),
Throws.TypeOf<ObjectDisposedException>());
}
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<Type, Delegate> _handlers = new();
public void OnCultNet<TMessage>(Func<TMessage, ICultNetSchemaServerPeer, Task> callback)
where TMessage : ICultNetSchemaMessage => _handlers[typeof(TMessage)] = callback;
public void RemoveCultNetMessageListener<TMessage>(Delegate callback)
where TMessage : ICultNetSchemaMessage
{
if (_handlers.TryGetValue(typeof(TMessage), out var current) && current == callback)
_handlers.Remove(typeof(TMessage));
}
public bool HasHandler<TMessage>() where TMessage : ICultNetSchemaMessage =>
_handlers.ContainsKey(typeof(TMessage));
public Task DispatchAsync<TMessage>(TMessage message, ICultNetSchemaServerPeer peer)
where TMessage : ICultNetSchemaMessage =>
((Func<TMessage, ICultNetSchemaServerPeer, Task>)_handlers[typeof(TMessage)])(message, peer);
}
private sealed class FakePeer : ICultNetSchemaServerPeer
{
public List<ICultNetSchemaMessage> Messages { get; } = new();
public void SendCultNet<TMessage>(TMessage message) where TMessage : ICultNetSchemaMessage =>
Messages.Add(message);
}
}
}