using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using PrivaPub.Infrastructure.Http; using System.Collections.Concurrent; namespace PrivaPub.Tests.Support { public sealed class Peer : IAsyncDisposable { readonly WebApplication _app; readonly ConcurrentDictionary _documents = new(); readonly ConcurrentDictionary _answers = new(); public int Port { get; } public string A => $"http://127.0.0.1:{Port}"; public string B => $"http://localhost:{Port}"; public ConcurrentQueue Requests { get; } = new(); Peer(WebApplication app, int port) { _app = app; Port = port; } public static async Task Start() { var builder = WebApplication.CreateSlimBuilder(); builder.WebHost.UseUrls("http://127.0.0.1:0"); var app = builder.Build(); Peer peer = default; app.Run(async context => { peer.Requests.Enqueue(new(context.Request.Method, context.Request.Path, context.Request.Headers["Signature"].ToString())); var key = context.Request.Path.Value; if (peer._answers.TryGetValue(key, out var answer)) { if (answer.Delay > TimeSpan.Zero) await Task.Delay(answer.Delay); context.Response.StatusCode = answer.Status; return; } if (!peer._documents.TryGetValue(key, out var document)) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } context.Response.ContentType = "application/activity+json"; await context.Response.WriteAsync(document.Replace("{A}", peer.A).Replace("{B}", peer.B)); }); await app.StartAsync(); peer = new Peer(app, new Uri(app.Urls.First()).Port); return peer; } public void Serve(string path, string json) => _documents[path] = json; public void Answer(string path, int status, TimeSpan delay = default) => _answers[path] = (status, delay); public static FederationHttp Http(IMemoryCache cache = default) { var options = new FederationOptions { AllowPrivateNetworks = true, AllowPlainHttp = true }; var services = new ServiceCollection(); services.AddHttpClient(FederationHttp.ClientName) .ConfigurePrimaryHttpMessageHandler(() => SafeHttpHandlerFactory.Create(options)); return new FederationHttp(services.BuildServiceProvider().GetRequiredService(), cache ?? new MemoryCache(new MemoryCacheOptions()), new StaticOptions(options), NullLogger.Instance); } public async ValueTask DisposeAsync() => await _app.DisposeAsync(); } public sealed record HttpRequestRecord(string Method, string Path, string Signature); public sealed class StaticOptions : IOptionsMonitor { public StaticOptions(T value) => CurrentValue = value; public T CurrentValue { get; } public T Get(string name) => CurrentValue; public IDisposable OnChange(Action listener) => default; } }