using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using GameCult.Networking; using NUnit.Framework; namespace GameCult.Mesh.Tests; [TestFixture] public sealed class CultMeshBodySessionTests { [Test] public async Task LiveBodySessionTransfersVerifiedGenerationWithoutSnapshotRecords() { var bytes = new byte[384_000]; for (var index = 0; index < bytes.Length; index++) bytes[index] = (byte)(index % 251); using var store = new CultMeshNetworkBodyStore(); var descriptor = store.Publish(Generation(sequence: 7), bytes); var server = new LoopbackServer(); using var bodyServer = new CultMeshBodyServer(server, store); var client = new LoopbackClient(server); var connector = new LoopbackConnector(client); using var discovery = new CultMeshDiscoveryService(new[] { new RouteSource() }); using var sessions = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessions); var provider = new CultMeshSessionBodyProvider( "aetheria.daemon", sessions, new CultMeshSessionTarget("aetheria", "aetheria.daemon")); var received = await provider.FetchAsync(descriptor); received.Should().Equal(bytes); client.BodyRequestCount.Should().Be(1); client.SnapshotRequestCount.Should().Be(0, "live body bytes must never enter database snapshot machinery"); connector.ConnectCount.Should().Be(1); } [Test] public async Task RudpBodySessionTransfersOneHotGenerationWithoutDatabaseRoundTrip() { var bytes = new byte[384_000]; for (var index = 0; index < bytes.Length; index++) bytes[index] = (byte)(index % 251); using var store = new CultMeshNetworkBodyStore(); var descriptor = store.Publish(Generation(sequence: 11), bytes); using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); using var server = new RudpCultNetSchemaServer(new RudpCultNetSchemaServerOptions { RuntimeId = "cultmesh-body-rudp-test", Socket = socket, MaxFragmentBytes = 1024, MaxPendingReliablePackets = 8192 }); var endpoint = $"rudp://127.0.0.1:{server.LocalEndPoint.Port}"; using var identityServer = new CultMeshSessionIdentityServer( server, "aetheria.daemon", new[] { "aetheria" }, new[] { CultMeshProtocols.Bodies.Value }, new[] { "aetheria.daemon@" + endpoint }); using var bodyServer = new CultMeshBodyServer(server, store); using var pumpCancellation = new CancellationTokenSource(); var pump = Task.Run(async () => { while (!pumpCancellation.IsCancellationRequested) { var progress = await server.PollAvailableAsync(256); server.PollResends(); if (progress.TransportItemsConsumed == 0) await Task.Delay(1, pumpCancellation.Token); } }); using var discovery = new CultMeshDiscoveryService(new[] { new RouteSource(endpoint) }); using var sessions = new CultMeshSessionManager(discovery, new ICultMeshTransportConnector[] { new CultMeshSchemaTransportConnector() }, CultMeshTestTrust.LocalSessions); var provider = new CultMeshSessionBodyProvider("aetheria.daemon", sessions, new CultMeshSessionTarget("aetheria", "aetheria.daemon"), new CultMeshSessionBodyProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(10) }); var elapsed = Stopwatch.StartNew(); byte[] received; try { received = await provider.FetchAsync(descriptor); } finally { pumpCancellation.Cancel(); try { await pump; } catch (OperationCanceledException) { } } elapsed.Stop(); received.Should().Equal(bytes); elapsed.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(5)); } [Test] public void NetworkBodyStoreRejectsGenerationMetadataThatDoesNotMatchCapability() { using var store = new CultMeshNetworkBodyStore(); var descriptor = store.Publish(Generation(sequence: 3), new byte[] { 1, 2, 3 }); var request = Request(descriptor); request.Sequence++; store.TryRead(request, DateTimeOffset.UtcNow, out _, out _).Should().BeFalse(); } [Test] public void BodyWireMessagesRoundTripThroughSchemaDispatcher() { using var store = new CultMeshNetworkBodyStore(); var descriptor = store.Publish(Generation(sequence: 9), new byte[] { 1, 2, 3 }); var request = Request(descriptor); var response = new CultMeshBodyReadResponseMessage { MessageId = request.MessageId, Found = true, CapabilityToken = descriptor.CapabilityToken, BodyId = descriptor.BodyId, ProducerEpoch = descriptor.ProducerEpoch, Sequence = descriptor.Sequence, SizeBytes = 3, SemanticHash = descriptor.SemanticHash, Payload = new byte[] { 1, 2, 3 } }; CultNetSchemaMessageSerialization.Deserialize(CultNetSchemaMessageSerialization.Serialize(request)) .Should().BeEquivalentTo(request); CultNetSchemaMessageSerialization.Deserialize(CultNetSchemaMessageSerialization.Serialize(response)) .Should().BeEquivalentTo(response); } private static CultMeshBodyGeneration Generation(long sequence) => new() { BodyId = "eve:entity-soa:aetheria.daemon:pilot", ProducerId = "aetheria.daemon", SchemaId = "gamecult.eve.entity_soa.body.v2", LayoutVersion = 3, Capacity = 4096, ProducerEpoch = 2, Sequence = sequence, Synchronization = CultMeshBodySynchronization.TripleBuffer, LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds() }; private static CultMeshBodyReadRequestMessage Request(CultMeshBodyDescriptor descriptor) => new() { MessageId = Guid.NewGuid().ToString("N"), CapabilityToken = descriptor.CapabilityToken, BodyId = descriptor.BodyId, BodySchemaId = descriptor.SchemaId, LayoutVersion = descriptor.LayoutVersion, ProducerEpoch = descriptor.ProducerEpoch, Sequence = descriptor.Sequence, ExpectedSizeBytes = descriptor.ByteSize, SemanticHash = descriptor.SemanticHash }; private sealed class RouteSource : ICultMeshLookupSource { private readonly string _endpoint; public RouteSource(string endpoint = "rudp://127.0.0.13:3076") => _endpoint = endpoint; public string SourceId => "test"; public Task> LookupAsync(CultMeshDiscoveryQuery query, CancellationToken cancellationToken = default) { var now = DateTimeOffset.UtcNow; return Task.FromResult>(new[] { new CultMeshDiscoveryObservation(new CultMeshVerseDescriptor("aetheria", "Aetheria", CultMeshVerseAuthorityModel.OperatorCluster, new CultMeshVerseCompatibility("cultmesh.v1", "rules"), new[] { _endpoint }, new[] { "aetheria.daemon" }), SourceId, now, now.AddMinutes(1), CultMeshDiscoveryTrust.Signed) }); } } private sealed class LoopbackConnector : ICultMeshTransportConnector { private readonly ICultNetSchemaClient _client; public LoopbackConnector(ICultNetSchemaClient client) => _client = client; public string ConnectorId => "loopback"; public int Priority => 0; public int ConnectCount { get; private set; } public bool CanConnect(CultMeshTransportCandidate candidate) => true; public Task ConnectAsync(CultMeshTransportCandidate candidate, CultMeshProtocolId protocol, CancellationToken cancellationToken = default) { protocol.Should().Be(CultMeshProtocols.Bodies); ConnectCount++; return Task.FromResult(_client); } } private sealed class LoopbackServer : ICultNetSchemaServer { private readonly Dictionary _handlers = new(); public void OnCultNet(Func callback) where T : ICultNetSchemaMessage => _handlers[typeof(T)] = callback; public void RemoveCultNetMessageListener(Delegate callback) where T : ICultNetSchemaMessage => _handlers.Remove(typeof(T)); public Task Dispatch(T message, ICultNetSchemaServerPeer peer) where T : ICultNetSchemaMessage => ((Func)_handlers[typeof(T)])(message, peer); } private sealed class LoopbackClient : ICultNetSchemaClient, ICultMeshVerifiedSchemaClient { private readonly LoopbackServer _server; private readonly Dictionary> _handlers = new(); public LoopbackClient(LoopbackServer server) => _server = server; public bool Connected => true; public int BodyRequestCount { get; private set; } public int SnapshotRequestCount { get; private set; } public void Connect(string host, int port) { } public void SendCultNet(T message) where T : ICultNetSchemaMessage { if (message is CultMeshBodyReadRequestMessage body) { BodyRequestCount++; _server.Dispatch(body, new LoopbackPeer(this)).GetAwaiter().GetResult(); } else if (message is CultNetSnapshotRequestMessage) SnapshotRequestCount++; } public void OnCultNet(Action callback) where T : ICultNetSchemaMessage { if (!_handlers.TryGetValue(typeof(T), out var values)) _handlers[typeof(T)] = values = new List(); values.Add(callback); } public void Emit(T message) where T : ICultNetSchemaMessage { if (_handlers.TryGetValue(typeof(T), out var values)) foreach (var value in values.ToArray()) ((Action)value)(message); } public void Dispose() { } public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) => verseId == "aetheria" && authorityRuntimeId == "aetheria.daemon"; } private sealed class LoopbackPeer : ICultNetSchemaServerPeer { private readonly LoopbackClient _client; public LoopbackPeer(LoopbackClient client) => _client = client; public void SendCultNet(T message) where T : ICultNetSchemaMessage => _client.Emit(message); } }