feat: add publisher and browser client SDK (#9)
quality-gate / quality (push) Successful in 1m6s
quality-gate / quality (push) Successful in 1m6s
Closes #9
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FinalFactory.Rendezvous.Client;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Tests.Client;
|
||||
|
||||
public sealed class RendezvousClientBehaviorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RegistrationRetriesWithTheSameIdempotentPayloadAndDisposesResponses()
|
||||
{
|
||||
TrackingContent unavailable = JsonContent(new ApiError
|
||||
{
|
||||
Code = RendezvousErrorCode.ServiceUnavailable,
|
||||
Message = "try later",
|
||||
RetryAfterSeconds = 1,
|
||||
});
|
||||
TrackingContent created = JsonContent(CreateRegistrationResponse());
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.ServiceUnavailable, unavailable),
|
||||
Response(HttpStatusCode.Created, created),
|
||||
new HttpResponseMessage(HttpStatusCode.NoContent));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RegisterSessionRequest request = CreateRegistrationRequest("stable-idempotency-key");
|
||||
RecordingDelay delay = new(() => request.DisplayName = "mutated during retry delay");
|
||||
RendezvousPublisherClient publisher = new(
|
||||
httpClient,
|
||||
new RendezvousClientOptions { JitterRatio = 0 },
|
||||
delay);
|
||||
|
||||
RendezvousClientResult<PublishedSession> result = await publisher.RegisterAsync(
|
||||
request,
|
||||
"publisher-credential");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, handler.RequestBodies.Count);
|
||||
Assert.Equal(handler.RequestBodies[0], handler.RequestBodies[1]);
|
||||
Assert.Contains("stable-idempotency-key", handler.RequestBodies[0], StringComparison.Ordinal);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1), Assert.Single(delay.Delays));
|
||||
Assert.True(unavailable.IsDisposed);
|
||||
Assert.True(created.IsDisposed);
|
||||
|
||||
using HttpResponseMessage stillOwnedByCaller = await httpClient.GetAsync("health");
|
||||
Assert.Equal(HttpStatusCode.NoContent, stillOwnedByCaller.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateUsesTheLeaseWithoutMutatingTheCallersRequest()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())),
|
||||
new HttpResponseMessage(HttpStatusCode.NoContent));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousPublisherClient publisher = new(httpClient);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistrationRequest("update-idempotency-key"),
|
||||
"publisher-credential"));
|
||||
UpdateSessionRequest update = new()
|
||||
{
|
||||
LeaseToken = "caller-placeholder",
|
||||
BuildVersion = "2.0.0",
|
||||
DisplayName = "updated",
|
||||
Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 4 },
|
||||
Metadata = new() { ["mode"] = "online-coop" },
|
||||
};
|
||||
|
||||
RendezvousClientResult<bool> result = await publisher.UpdateAsync(
|
||||
session,
|
||||
update,
|
||||
"publisher-credential");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal("caller-placeholder", update.LeaseToken);
|
||||
Assert.Contains("lease-token", handler.RequestBodies[1], StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("caller-placeholder", handler.RequestBodies[1], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GatewayFailureIsRetriedForSafeBrowserReads()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
new HttpResponseMessage(HttpStatusCode.BadGateway),
|
||||
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse())));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RecordingDelay delay = new();
|
||||
RendezvousSessionBrowserClient browser = new(
|
||||
httpClient,
|
||||
new RendezvousClientOptions { JitterRatio = 0 },
|
||||
delay);
|
||||
|
||||
RendezvousClientResult<BrowseSessionsResponse> result = await browser.BrowseAsync(new()
|
||||
{
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ProtocolVersion = 7,
|
||||
});
|
||||
|
||||
Assert.True(result.IsSuccess, result.Message);
|
||||
Assert.Equal(2, handler.RequestUris.Count);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(200), Assert.Single(delay.Delays));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuccessResultRequiresAValue()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => RendezvousClientResult.Success<string>(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BrowseAllFollowsCursorsWithoutMutatingTheCallersRequest()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse
|
||||
{
|
||||
Items = [CreateListing("00000000-0000-0000-0000-000000000001")],
|
||||
NextCursor = "next page+token",
|
||||
})),
|
||||
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse
|
||||
{
|
||||
Items = [CreateListing("00000000-0000-0000-0000-000000000002")],
|
||||
})));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousSessionBrowserClient browser = new(httpClient);
|
||||
BrowseSessionsRequest request = new()
|
||||
{
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ProtocolVersion = 7,
|
||||
PageSize = 1,
|
||||
};
|
||||
|
||||
RendezvousClientResult<IReadOnlyList<SessionListing>> result = await browser.BrowseAllAsync(request);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value!.Count);
|
||||
Assert.Null(request.Cursor);
|
||||
Assert.DoesNotContain("cursor=", handler.RequestUris[0].Query, StringComparison.Ordinal);
|
||||
Assert.Contains("cursor=next%20page%2Btoken", handler.RequestUris[1].Query, StringComparison.Ordinal);
|
||||
Assert.Contains("gameId=space-game", handler.RequestUris[0].Query, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LeaseMaintainerReportsLeaseLoss()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())),
|
||||
Response(HttpStatusCode.Gone, JsonContent(new ApiError
|
||||
{
|
||||
Code = RendezvousErrorCode.Expired,
|
||||
Message = "lease expired",
|
||||
})));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RecordingDelay delay = new();
|
||||
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistrationRequest("lease-loss-key"),
|
||||
"publisher-credential"));
|
||||
await using SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
|
||||
session,
|
||||
"publisher-credential");
|
||||
bool eventRaised = false;
|
||||
maintainer.LeaseLost += (_, _) => eventRaised = true;
|
||||
|
||||
LeaseMaintenanceResult result = await maintainer.RunAsync();
|
||||
|
||||
Assert.Equal(LeaseMaintenanceStopReason.LeaseLost, result.Reason);
|
||||
Assert.Equal(RendezvousErrorCode.Expired, result.Error);
|
||||
Assert.True(eventRaised);
|
||||
Assert.Equal(TimeSpan.FromSeconds(15), Assert.Single(delay.Delays));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposingLeaseMaintainerCancelsItsWaitAndDoesNotRenew()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
BlockingDelay delay = new();
|
||||
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistrationRequest("dispose-key"),
|
||||
"publisher-credential"));
|
||||
SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
|
||||
session,
|
||||
"publisher-credential");
|
||||
Task<LeaseMaintenanceResult> active = maintainer.RunAsync();
|
||||
await delay.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
await maintainer.DisposeAsync();
|
||||
LeaseMaintenanceResult result = await active;
|
||||
|
||||
Assert.Equal(LeaseMaintenanceStopReason.Disposed, result.Reason);
|
||||
Assert.Single(handler.RequestUris);
|
||||
await Assert.ThrowsAsync<ObjectDisposedException>(() => maintainer.RunAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallerCancellationStopsLeaseMaintenanceWithoutRenewing()
|
||||
{
|
||||
ScriptedHandler handler = new(
|
||||
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())));
|
||||
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
BlockingDelay delay = new();
|
||||
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistrationRequest("cancel-key"),
|
||||
"publisher-credential"));
|
||||
await using SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
|
||||
session,
|
||||
"publisher-credential");
|
||||
using CancellationTokenSource cancellation = new();
|
||||
Task<LeaseMaintenanceResult> active = maintainer.RunAsync(cancellation.Token);
|
||||
await delay.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
await cancellation.CancelAsync();
|
||||
LeaseMaintenanceResult result = await active;
|
||||
|
||||
Assert.Equal(LeaseMaintenanceStopReason.Cancelled, result.Reason);
|
||||
Assert.Single(handler.RequestUris);
|
||||
}
|
||||
|
||||
private static T AssertSuccess<T>(RendezvousClientResult<T> result)
|
||||
{
|
||||
Assert.True(result.IsSuccess, result.Message);
|
||||
return Assert.IsType<T>(result.Value);
|
||||
}
|
||||
|
||||
private static RegisterSessionRequest CreateRegistrationRequest(string idempotencyKey) => new()
|
||||
{
|
||||
IdempotencyKey = idempotencyKey,
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
RegionId = new("eu-central"),
|
||||
ProtocolVersion = 7,
|
||||
BuildVersion = "1.0.0",
|
||||
DisplayName = "SDK host",
|
||||
Visibility = ListingVisibility.Public,
|
||||
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 4 },
|
||||
};
|
||||
|
||||
private static RegisterSessionResponse CreateRegistrationResponse() => new()
|
||||
{
|
||||
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000010")),
|
||||
LeaseId = new(Guid.Parse("00000000-0000-0000-0000-000000000011")),
|
||||
LeaseToken = "lease-token",
|
||||
HostPresenceHandle = new(Guid.Parse("00000000-0000-0000-0000-000000000012")),
|
||||
HostPresenceCapability = "presence-capability",
|
||||
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
LeaseRenewAfterSeconds = 15,
|
||||
HostPresenceRefreshAfterSeconds = 10,
|
||||
};
|
||||
|
||||
private static SessionListing CreateListing(string id) => new()
|
||||
{
|
||||
ListingId = new(Guid.Parse(id)),
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
RegionId = new("eu-central"),
|
||||
ProtocolVersion = 7,
|
||||
BuildVersion = "1.0.0",
|
||||
DisplayName = "host",
|
||||
Visibility = ListingVisibility.Public,
|
||||
PublisherTrustMode = PublisherTrustMode.ManagedDedicated,
|
||||
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 4 },
|
||||
};
|
||||
|
||||
private static TrackingContent JsonContent<T>(T value) => new(
|
||||
JsonSerializer.SerializeToUtf8Bytes(value, ContractJson.Options));
|
||||
|
||||
private static HttpResponseMessage Response(HttpStatusCode status, HttpContent content) => new(status)
|
||||
{
|
||||
Content = content,
|
||||
};
|
||||
|
||||
private sealed class ScriptedHandler(params HttpResponseMessage[] responses) : HttpMessageHandler
|
||||
{
|
||||
private readonly Queue<HttpResponseMessage> _responses = new(responses);
|
||||
|
||||
internal List<string> RequestBodies { get; } = [];
|
||||
internal List<Uri> RequestUris { get; } = [];
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
RequestUris.Add(request.RequestUri!);
|
||||
RequestBodies.Add(request.Content is null
|
||||
? string.Empty
|
||||
: await request.Content.ReadAsStringAsync(cancellationToken));
|
||||
return _responses.Count > 0
|
||||
? _responses.Dequeue()
|
||||
: throw new InvalidOperationException("No scripted response remains.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TrackingContent(byte[] bytes) : HttpContent
|
||||
{
|
||||
internal bool IsDisposed { get; private set; }
|
||||
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
|
||||
stream.WriteAsync(bytes).AsTask();
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = bytes.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
IsDisposed = true;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingDelay(Action? onDelay = null) : IRendezvousDelay
|
||||
{
|
||||
internal List<TimeSpan> Delays { get; } = [];
|
||||
|
||||
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Delays.Add(delay);
|
||||
onDelay?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockingDelay : IRendezvousDelay
|
||||
{
|
||||
internal TaskCompletionSource Started { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
Started.TrySetResult();
|
||||
return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using FinalFactory.Rendezvous.Client;
|
||||
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.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<PublishedSession> sessions = [];
|
||||
|
||||
for (int index = 0; index < 3; index++)
|
||||
{
|
||||
RendezvousClientResult<PublishedSession> 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<StoredListing> 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<RenewLeaseResponse> 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<bool> 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<SessionListing> 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<bool> 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<T>(RendezvousClientResult<T> result)
|
||||
{
|
||||
Assert.True(result.IsSuccess, result.Message);
|
||||
return Assert.IsAssignableFrom<T>(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<ClientTestHost> 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.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>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user