using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; using System.Text.Json; using FinalFactory.Rendezvous.Contracts; 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.Sessions; public sealed class SessionHttpEndpointTests { [Fact] public async Task AuthenticatedHttpLifecycleReturnsStableContractsAndStatuses() { 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 publisherCredential = 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.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(); await using WebApplication app = builder.Build(); app.UseExceptionHandler(); app.MapRendezvousContractEndpoints(); await app.StartAsync(); IServer server = app.Services.GetRequiredService(); string address = Assert.Single(server.Features.Get()!.Addresses); using HttpClient client = new() { BaseAddress = new Uri(address) }; RegisterSessionRequest registration = new() { IdempotencyKey = "http-register-1", GameId = new("space-game"), EnvironmentId = new("production"), RegionId = new("eu-central"), ProtocolVersion = 7, BuildVersion = "1.4.2", DisplayName = "HTTP host", Visibility = ListingVisibility.Public, Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 8 }, Metadata = new Dictionary(StringComparer.Ordinal) { ["mode"] = "co-op", }, }; HttpResponseMessage unauthenticated = await client.PostAsJsonAsync( "/v1/sessions", registration, ContractJson.Options); Assert.Equal(HttpStatusCode.Unauthorized, unauthenticated.StatusCode); Assert.Equal("Bearer", Assert.Single(unauthenticated.Headers.WwwAuthenticate).Scheme); ApiError? authenticationError = await unauthenticated.Content.ReadFromJsonAsync( ContractJson.Options); Assert.Equal(RendezvousErrorCode.AuthenticationRequired, authenticationError!.Code); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", publisherCredential); string invalidJson = JsonSerializer.Serialize(registration, ContractJson.Options) .Replace("\"public\"", "\"futureVisibility\"", StringComparison.Ordinal); HttpResponseMessage invalid = await client.PostAsync( "/v1/sessions", new StringContent(invalidJson, Encoding.UTF8, "application/json")); Assert.Equal(HttpStatusCode.BadRequest, invalid.StatusCode); ApiError? invalidError = await invalid.Content.ReadFromJsonAsync(ContractJson.Options); Assert.Equal(RendezvousErrorCode.InvalidRequest, invalidError!.Code); HttpResponseMessage created = await client.PostAsJsonAsync( "/v1/sessions", registration, ContractJson.Options); Assert.Equal(HttpStatusCode.Created, created.StatusCode); RegisterSessionResponse? session = await created.Content.ReadFromJsonAsync( ContractJson.Options); Assert.NotNull(session); Assert.Equal($"/v1/sessions/{session.ListingId}", created.Headers.Location!.OriginalString); Assert.True(capabilities.TryFingerprint( session.HostPresenceCapability, out SecretFingerprint presenceFingerprint)); store.BindHostPresence(new( session.HostPresenceHandle, presenceFingerprint, new(AddressFamilyKind.Ipv4, "203.0.113.80", 41_000), null)); BrowseSessionsResponse? browser = await client.GetFromJsonAsync( "/v1/sessions?contractVersion=1&gameId=space-game&environmentId=production&protocolVersion=7®ionId=eu-central&pageSize=10&excludeFull=true", ContractJson.Options); Assert.Equal(session.ListingId, Assert.Single(browser!.Items).ListingId); GetSessionResponse? direct = await client.GetFromJsonAsync( $"/v1/sessions/{session.ListingId}?contractVersion=1&gameId=space-game&environmentId=production&protocolVersion=7", ContractJson.Options); Assert.Equal(session.ListingId, direct!.Session.ListingId); HttpResponseMessage renewed = await client.PostAsJsonAsync( $"/v1/sessions/{session.ListingId}/renew", new RenewLeaseRequest { LeaseToken = session.LeaseToken }, ContractJson.Options); Assert.Equal(HttpStatusCode.OK, renewed.StatusCode); Assert.NotNull(await renewed.Content.ReadFromJsonAsync(ContractJson.Options)); HttpResponseMessage updated = await client.PutAsJsonAsync( $"/v1/sessions/{session.ListingId}", new UpdateSessionRequest { LeaseToken = session.LeaseToken, BuildVersion = "1.4.3", DisplayName = "HTTP host updated", Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 8 }, Metadata = new Dictionary { ["mode"] = "co-op" }, }, ContractJson.Options); Assert.Equal(HttpStatusCode.NoContent, updated.StatusCode); using HttpRequestMessage deleteRequest = new( HttpMethod.Delete, $"/v1/sessions/{session.ListingId}") { Content = JsonContent.Create( new DeleteSessionRequest { LeaseToken = session.LeaseToken }, options: ContractJson.Options), }; HttpResponseMessage deleted = await client.SendAsync(deleteRequest); Assert.Equal(HttpStatusCode.NoContent, deleted.StatusCode); Assert.Equal(StoreResultCode.NotFound, store.GetListing(session.ListingId, false).Code); await app.StopAsync(); } }