using FinalFactory.Rendezvous.Client; using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.Abuse; using FinalFactory.Rendezvous.Server.Browser; using FinalFactory.Rendezvous.Server.Http; using FinalFactory.Rendezvous.Server.Provisioning; using FinalFactory.Rendezvous.Server.Sessions; using FinalFactory.Rendezvous.Server.State; using FinalFactory.Rendezvous.Tests.Provisioning; using FinalFactory.Rendezvous.Tests.State; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace FinalFactory.Rendezvous.Tests.Client; public sealed class RendezvousClientIntegrationTests { [Fact] public async Task PublisherAndBrowserClientsCompleteTheRealSessionLifecycleAndPaging() { await using ClientTestHost host = await ClientTestHost.StartAsync(); RendezvousPublisherClient publisher = new(host.HttpClient); RendezvousSessionBrowserClient browser = new(host.HttpClient); List sessions = []; for (int index = 0; index < 3; index++) { RendezvousClientResult registered = await publisher.RegisterAsync( CreateRegistration(index), host.PublisherCredential); PublishedSession session = AssertSuccess(registered); sessions.Add(session); Assert.True(host.Capabilities.TryFingerprint( session.HostPresenceCapability, out SecretFingerprint fingerprint)); StoreResult bound = host.Store.BindHostPresence(new( session.HostPresenceHandle, fingerprint, new(AddressFamilyKind.Ipv4, $"203.0.113.{80 + index}", 41_000 + index), null)); Assert.Equal(StoreResultCode.Success, bound.Code); } PublishedSession first = sessions[0]; RendezvousClientResult renewed = await publisher.RenewAsync( first, host.PublisherCredential); Assert.True(renewed.IsSuccess, renewed.Message); Assert.Equal(renewed.Value!.ExpiresAt, first.ExpiresAt); UpdateSessionRequest update = new() { BuildVersion = "2.0.0", DisplayName = "SDK host updated", Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 8 }, Metadata = new() { ["mode"] = "online-coop" }, }; RendezvousClientResult updated = await publisher.UpdateAsync( first, update, host.PublisherCredential); Assert.True(updated.IsSuccess, updated.Message); BrowseSessionsRequest browseRequest = new() { GameId = new("space-game"), EnvironmentId = new("production"), RegionId = new("eu-central"), ProtocolVersion = 7, PageSize = 1, ExcludeFull = true, }; IReadOnlyList listings = AssertSuccess( await browser.BrowseAllAsync(browseRequest)); Assert.Equal(3, listings.Count); Assert.Equal("SDK host updated", listings.Single(item => item.ListingId == first.ListingId).DisplayName); GetSessionResponse direct = AssertSuccess(await browser.GetAsync( first.ListingId, new("space-game"), new("production"), 7)); Assert.Equal("2.0.0", direct.Session.BuildVersion); foreach (PublishedSession session in sessions) { RendezvousClientResult deregistered = await publisher.DeregisterAsync( session, host.PublisherCredential); Assert.True(deregistered.IsSuccess, deregistered.Message); Assert.Equal(StoreResultCode.NotFound, host.Store.GetListing(session.ListingId, false).Code); } } private static T AssertSuccess(RendezvousClientResult result) { Assert.True(result.IsSuccess, result.Message); return Assert.IsAssignableFrom(result.Value); } private static RegisterSessionRequest CreateRegistration(int index) => new() { IdempotencyKey = $"sdk-integration-{index}", GameId = new("space-game"), EnvironmentId = new("production"), RegionId = new("eu-central"), ProtocolVersion = 7, BuildVersion = "1.0.0", DisplayName = $"SDK host {index}", Visibility = ListingVisibility.Public, Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 8 }, Metadata = new() { ["mode"] = "online-coop" }, }; private sealed class ClientTestHost : IAsyncDisposable { private readonly WebApplication _application; private ClientTestHost( WebApplication application, HttpClient httpClient, InMemoryEphemeralRendezvousStore store, EphemeralCapabilityIssuer capabilities, string publisherCredential) { _application = application; HttpClient = httpClient; Store = store; Capabilities = capabilities; PublisherCredential = publisherCredential; } internal HttpClient HttpClient { get; } internal InMemoryEphemeralRendezvousStore Store { get; } internal EphemeralCapabilityIssuer Capabilities { get; } internal string PublisherCredential { get; } internal static async Task StartAsync() { ManualRendezvousClock clock = new(ProvisioningTestData.Now); EphemeralStoreOptions stateOptions = new(); InMemoryEphemeralRendezvousStore store = new(stateOptions, clock, clock); EphemeralCapabilityIssuer capabilities = new(); ProvisioningRuntime provisioning = ProvisioningRuntime.Create( ProvisioningTestData.CreateOptions(), ProvisioningTestData.CreateSecrets("secret-1"), clock.UtcNow); DedicatedPublisherPrincipal principal = ProvisioningTestData.CreateDedicatedPublisher(); string credential = provisioning.Credentials.Issue(principal, clock.UtcNow); WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseUrls("http://127.0.0.1:0"); builder.Services.ConfigureHttpJsonOptions(static options => ContractJson.Configure(options.SerializerOptions)); builder.Services.Configure(static options => options.ThrowOnBadRequest = true); builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler(); builder.Services.AddOptions(); builder.Services.AddSingleton(); builder.Services.AddSingleton(provisioning); builder.Services.AddSingleton(provisioning.Credentials); builder.Services.AddSingleton(provisioning.PublisherAuthorization); builder.Services.AddSingleton(store); builder.Services.AddSingleton(clock); builder.Services.AddSingleton(capabilities); builder.Services.AddSingleton(capabilities); builder.Services.AddSingleton(SessionLeaseTiming.From(stateOptions)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); WebApplication app = builder.Build(); app.UseExceptionHandler(); app.UseMiddleware(); app.MapRendezvousContractEndpoints(); await app.StartAsync(); IServer server = app.Services.GetRequiredService(); string address = Assert.Single(server.Features.Get()!.Addresses); return new( app, new HttpClient { BaseAddress = new Uri(address) }, store, capabilities, credential); } public async ValueTask DisposeAsync() { HttpClient.Dispose(); await _application.StopAsync(); await _application.DisposeAsync(); } } }