-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEnd2EndSpecBase.cs
More file actions
219 lines (179 loc) · 7.3 KB
/
Copy pathEnd2EndSpecBase.cs
File metadata and controls
219 lines (179 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Akka.Actor;
using Akka.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Servus.Akka.Transport;
using GaudiHTTP.Client;
using GaudiHTTP.Server;
using GaudiHTTP.Tests.Shared;
using QuicListenerOptionsServus = Servus.Akka.Transport.QuicListenerOptions;
namespace GaudiHTTP.IntegrationTests.End2End.Shared;
public abstract class End2EndSpecBase : IAsyncLifetime
{
// One RSA keygen per process instead of one per TLS test — keygen is CPU-heavy
// and amplifies starvation when collections run in parallel on small CI runners.
private static readonly Lazy<X509Certificate2> SharedCertificate =
new(() => CreateSelfSignedCertificate("127.0.0.1"), LazyThreadSafetyMode.ExecutionAndPublication);
private WebApplication? _app;
private IGaudiHttpClient? _client;
private Microsoft.Extensions.DependencyInjection.ServiceProvider? _clientProvider;
private X509Certificate2? _cert;
protected abstract Version ProtocolVersion { get; }
protected abstract void ConfigureEndpoints(WebApplication app);
protected virtual bool UseTls => ProtocolVersion.Major >= 2;
protected virtual void ConfigureServer(GaudiServerOptions options, ushort port, X509Certificate2? cert)
{
if (ProtocolVersion.Major == 3)
{
if (!QuicConnection.IsSupported)
{
return;
}
var quicOptions = new QuicListenerOptionsServus
{
Host = "127.0.0.1",
Port = port,
ServerCertificate = cert!,
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 }
};
options.Bind(quicOptions);
}
else if (ProtocolVersion.Major == 2)
{
options.ListenLocalhost(port, listen =>
{
listen.UseHttps(cert!);
listen.Protocols = HttpProtocols.Http2;
});
}
else
{
options.Bind(new TcpListenerOptions
{
Host = "127.0.0.1",
Port = port
});
}
}
protected virtual void ConfigureClientOptions(GaudiClientOptions options)
{
}
/// <summary>
/// Global client timeout. Keep the default low — several specs rely on it as a backstop
/// well below their watchdogs. Bulk-transfer stress specs override this with a higher
/// value so legitimate slow transfers under CI contention don't trip it.
/// </summary>
protected virtual TimeSpan ClientTimeout => TimeSpan.FromSeconds(10);
protected IGaudiHttpClient Client => _client!;
protected string BaseUri { get; private set; } = string.Empty;
protected CancellationToken CancellationToken => TestContext.Current.CancellationToken;
public async ValueTask InitializeAsync()
{
if (ProtocolVersion.Major == 3 && !QuicConnection.IsSupported)
{
Assert.Skip("QUIC not available on this platform");
}
var needsTls = UseTls;
if (needsTls)
{
_cert = SharedCertificate.Value;
}
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
// Bind port 0 and read the real port back after start — probing for a free
// port and rebinding it races with parallel tests (and parallel test modules).
builder.Host.UseGaudiHttp(options =>
{
ConfigureServer(options, 0, _cert);
});
_app = builder.Build();
ConfigureEndpoints(_app);
await _app.StartAsync();
var scheme = needsTls ? "https" : "http";
BaseUri = $"{scheme}://127.0.0.1:{ResolveBoundPort(_app)}";
var services = new ServiceCollection();
var diSetup = DependencyResolverSetup.Create(services.BuildServiceProvider());
var bootstrap = BootstrapSetup.Create();
var system = ActorSystem.Create($"e2e-client-{Guid.NewGuid():N}", bootstrap.And(diSetup));
services.AddSingleton(system);
var clientOptions = new GaudiClientOptions
{
BaseAddress = new Uri(BaseUri),
DangerousAcceptAnyServerCertificate = needsTls
};
ConfigureClientOptions(clientOptions);
services.AddGaudiHttpClient();
services.Replace(ServiceDescriptor.Singleton<IOptionsFactory<GaudiClientOptions>>(
new FixedOptionsFactory(clientOptions)));
_clientProvider = services.BuildServiceProvider();
var factory = _clientProvider.GetRequiredService<IGaudiHttpClientFactory>();
_client = factory.CreateClient(string.Empty);
_client.DefaultRequestVersion = ProtocolVersion;
_client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
_client.Timeout = ClientTimeout;
}
public virtual async ValueTask DisposeAsync()
{
// Dump before teardown so the ring still shows the failed request's frames.
await FaultTraceDump.DumpIfTestFailedAsync();
_client?.Dispose();
if (_app is not null)
{
await _app.StopAsync();
await _app.DisposeAsync();
}
if (_clientProvider is not null)
{
var system = _clientProvider.GetService<ActorSystem>();
if (system is not null)
{
await system.Terminate().WaitAsync(TimeSpan.FromSeconds(10));
await system.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(5));
}
await _clientProvider.DisposeAsync();
}
}
protected static X509Certificate2 CreateSelfSignedCertificate(string cn)
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest(
$"CN={cn}",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName(cn);
sanBuilder.AddIpAddress(IPAddress.Parse("127.0.0.1"));
request.CertificateExtensions.Add(sanBuilder.Build());
var cert = request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddMinutes(-1),
DateTimeOffset.UtcNow.AddDays(1));
return X509CertificateLoader.LoadPkcs12(
cert.Export(X509ContentType.Pfx),
null,
X509KeyStorageFlags.Exportable);
}
private static int ResolveBoundPort(WebApplication app)
{
var addresses = app.Services.GetRequiredService<IServer>()
.Features.Get<IServerAddressesFeature>()!
.Addresses;
return new Uri(addresses.First()).Port;
}
private sealed class FixedOptionsFactory(GaudiClientOptions options) : IOptionsFactory<GaudiClientOptions>
{
public GaudiClientOptions Create(string name) => options;
}
}