feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s
quality-gate / quality (push) Successful in 55s
Closes #7
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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.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<RouteHandlerOptions>(static options =>
|
||||
options.ThrowOnBadRequest = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddExceptionHandler<RendezvousExceptionHandler>();
|
||||
builder.Services.AddSingleton(provisioning);
|
||||
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>();
|
||||
await using 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);
|
||||
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<string, string>(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<ApiError>(
|
||||
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<ApiError>(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<RegisterSessionResponse>(
|
||||
ContractJson.Options);
|
||||
Assert.NotNull(session);
|
||||
Assert.Equal($"/v1/sessions/{session.ListingId}", created.Headers.Location!.OriginalString);
|
||||
|
||||
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<RenewLeaseResponse>(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<string, string> { ["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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user