205 lines
9.6 KiB
C#
205 lines
9.6 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using FinalFactory.Rendezvous.Client;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.Browser;
|
|
using FinalFactory.Rendezvous.Server.Http;
|
|
using FinalFactory.Rendezvous.Server.JoinAttempts;
|
|
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.JoinAttempts;
|
|
|
|
public sealed class JoinAttemptHttpEndpointTests
|
|
{
|
|
[Fact]
|
|
public async Task ClientCreatesHostPollsAndCapabilityCancelsAnAttemptOverHttp()
|
|
{
|
|
await using JoinHttpTestHost host = await JoinHttpTestHost.StartAsync();
|
|
RendezvousPublisherClient publisher = new(host.HttpClient);
|
|
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
|
CreateRegistration(),
|
|
host.PublisherCredential));
|
|
Assert.True(host.Capabilities.TryFingerprint(
|
|
session.HostPresenceCapability,
|
|
out SecretFingerprint presenceFingerprint));
|
|
Assert.True(host.Store.BindHostPresence(new(
|
|
session.HostPresenceHandle,
|
|
presenceFingerprint,
|
|
new(AddressFamilyKind.Ipv4, "203.0.113.80", 41_000),
|
|
null)).Succeeded);
|
|
CreateJoinAttemptRequest request = new()
|
|
{
|
|
IdempotencyKey = "http-join-1",
|
|
GameId = new("space-game"),
|
|
EnvironmentId = new("production"),
|
|
ListingId = session.ListingId,
|
|
ProtocolVersion = 7,
|
|
};
|
|
|
|
using HttpResponseMessage createdResponse = await host.HttpClient.PostAsJsonAsync(
|
|
"v1/join-attempts",
|
|
request,
|
|
ContractJson.Options);
|
|
Assert.Equal(HttpStatusCode.Created, createdResponse.StatusCode);
|
|
CreateJoinAttemptResponse created = Assert.IsType<CreateJoinAttemptResponse>(
|
|
await createdResponse.Content.ReadFromJsonAsync<CreateJoinAttemptResponse>(ContractJson.Options));
|
|
|
|
using HttpRequestMessage pollRequest = new(
|
|
HttpMethod.Get,
|
|
$"v1/sessions/{session.ListingId}/join-attempts?contractVersion=1&pageSize=10");
|
|
pollRequest.Headers.Add("X-Rendezvous-Lease-Token", session.LeaseToken);
|
|
using HttpResponseMessage pollResponse = await host.HttpClient.SendAsync(pollRequest);
|
|
Assert.Equal(HttpStatusCode.OK, pollResponse.StatusCode);
|
|
BrowseHostJoinAttemptsResponse polled = Assert.IsType<BrowseHostJoinAttemptsResponse>(
|
|
await pollResponse.Content.ReadFromJsonAsync<BrowseHostJoinAttemptsResponse>(ContractJson.Options));
|
|
HostJoinAttempt hostAttempt = Assert.Single(polled.Items);
|
|
Assert.Equal(created.AttemptId, hostAttempt.AttemptId);
|
|
Assert.NotEqual(created.ClientPunchCapability, hostAttempt.HostPunchCapability);
|
|
|
|
using HttpResponseMessage missingCapability = await host.HttpClient.DeleteAsync(
|
|
$"v1/join-attempts/{created.AttemptId}");
|
|
Assert.Equal(HttpStatusCode.BadRequest, missingCapability.StatusCode);
|
|
ApiError missingCapabilityError = Assert.IsType<ApiError>(
|
|
await missingCapability.Content.ReadFromJsonAsync<ApiError>(ContractJson.Options));
|
|
Assert.Equal(RendezvousErrorCode.InvalidRequest, missingCapabilityError.Code);
|
|
|
|
using HttpRequestMessage unauthorizedCancel = new(
|
|
HttpMethod.Delete,
|
|
$"v1/join-attempts/{created.AttemptId}");
|
|
unauthorizedCancel.Headers.Add(
|
|
"X-Rendezvous-Client-Punch-Capability",
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
|
using HttpResponseMessage unauthorized = await host.HttpClient.SendAsync(unauthorizedCancel);
|
|
Assert.Equal(HttpStatusCode.NotFound, unauthorized.StatusCode);
|
|
|
|
using HttpRequestMessage cancelRequest = new(
|
|
HttpMethod.Delete,
|
|
$"v1/join-attempts/{created.AttemptId}");
|
|
cancelRequest.Headers.Add(
|
|
"X-Rendezvous-Client-Punch-Capability",
|
|
created.ClientPunchCapability);
|
|
using HttpResponseMessage cancelled = await host.HttpClient.SendAsync(cancelRequest);
|
|
Assert.Equal(HttpStatusCode.NoContent, cancelled.StatusCode);
|
|
|
|
using HttpRequestMessage emptyPollRequest = new(
|
|
HttpMethod.Get,
|
|
$"v1/sessions/{session.ListingId}/join-attempts?contractVersion=1&pageSize=10");
|
|
emptyPollRequest.Headers.Add("X-Rendezvous-Lease-Token", session.LeaseToken);
|
|
using HttpResponseMessage emptyPollResponse = await host.HttpClient.SendAsync(emptyPollRequest);
|
|
BrowseHostJoinAttemptsResponse empty = Assert.IsType<BrowseHostJoinAttemptsResponse>(
|
|
await emptyPollResponse.Content.ReadFromJsonAsync<BrowseHostJoinAttemptsResponse>(ContractJson.Options));
|
|
Assert.Empty(empty.Items);
|
|
}
|
|
|
|
private static T AssertSuccess<T>(RendezvousClientResult<T> result)
|
|
{
|
|
Assert.True(result.IsSuccess, result.Message);
|
|
return Assert.IsAssignableFrom<T>(result.Value);
|
|
}
|
|
|
|
private static RegisterSessionRequest CreateRegistration() => new()
|
|
{
|
|
IdempotencyKey = "join-http-host",
|
|
GameId = new("space-game"),
|
|
EnvironmentId = new("production"),
|
|
RegionId = new("eu-central"),
|
|
ProtocolVersion = 7,
|
|
BuildVersion = "1.0.0",
|
|
DisplayName = "Join HTTP host",
|
|
Visibility = ListingVisibility.Public,
|
|
Capacity = new() { CurrentPlayers = 8, MaximumPlayers = 8 },
|
|
Metadata = new() { ["mode"] = "online-coop" },
|
|
};
|
|
|
|
private sealed class JoinHttpTestHost : IAsyncDisposable
|
|
{
|
|
private readonly WebApplication _application;
|
|
|
|
private JoinHttpTestHost(
|
|
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<JoinHttpTestHost> 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<RouteHandlerOptions>(static options =>
|
|
options.ThrowOnBadRequest = true);
|
|
builder.Services.AddProblemDetails();
|
|
builder.Services.AddExceptionHandler<RendezvousExceptionHandler>();
|
|
builder.Services.AddSingleton(provisioning);
|
|
builder.Services.AddSingleton(provisioning.Policies);
|
|
builder.Services.AddSingleton(provisioning.Credentials);
|
|
builder.Services.AddSingleton(provisioning.PublisherAuthorization);
|
|
builder.Services.AddSingleton<IEphemeralRendezvousStore>(store);
|
|
builder.Services.AddSingleton<IWallClock>(clock);
|
|
builder.Services.AddSingleton(capabilities);
|
|
builder.Services.AddSingleton<ISessionCapabilityService>(capabilities);
|
|
builder.Services.AddSingleton(SessionLeaseTiming.From(stateOptions));
|
|
builder.Services.AddSingleton<SessionLeaseService>();
|
|
builder.Services.AddSingleton<SessionBrowserCursorCodec>();
|
|
builder.Services.AddSingleton<SessionBrowserService>();
|
|
builder.Services.AddSingleton<JoinAttemptCursorCodec>();
|
|
builder.Services.AddSingleton<JoinAttemptService>();
|
|
|
|
WebApplication app = builder.Build();
|
|
app.UseExceptionHandler();
|
|
app.MapRendezvousContractEndpoints();
|
|
await app.StartAsync();
|
|
IServer server = app.Services.GetRequiredService<IServer>();
|
|
string address = Assert.Single(server.Features.Get<IServerAddressesFeature>()!.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();
|
|
}
|
|
}
|
|
}
|