[Templates] Diagnostics improvements and certificate fixes (#21493)

* Move template specific helpers out of shared and into templates
* More debug info, fix casing issues
* Keep test skipped
* Undo lock changes
* Standarize retry logic
* Tweak certificate validation code
This commit is contained in:
Javier Calvarro Nelson 2020-05-06 04:39:51 +02:00 committed by GitHub
parent 88dbfaa2ab
commit d6409371ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 42 additions and 32 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- Shared testing infrastructure for running E2E tests using selenium --> <!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" /> <Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" />
@ -28,6 +28,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="template-baselines.json" /> <EmbeddedResource Include="template-baselines.json" />
<Compile Include="$(SharedSourceRoot)Process\*.cs" LinkBase="shared\Process" /> <Compile Include="$(SharedSourceRoot)Process\*.cs" LinkBase="shared\Process" />
<Compile Include="..\Shared\**" LinkBase="Helpers" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,6 +8,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom.Html; using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html; using AngleSharp.Parser.Html;
@ -33,6 +34,7 @@ namespace Templates.Test.Helpers
private string _certificatePath; private string _certificatePath;
private string _certificatePassword = Guid.NewGuid().ToString(); private string _certificatePassword = Guid.NewGuid().ToString();
private string _certificateThumbprint;
internal readonly Uri ListeningUri; internal readonly Uri ListeningUri;
internal ProcessEx Process { get; } internal ProcessEx Process { get; }
@ -46,22 +48,21 @@ namespace Templates.Test.Helpers
bool hasListeningUri = true, bool hasListeningUri = true,
ILogger logger = null) ILogger logger = null)
{ {
_certificatePath = Path.Combine(workingDirectory, $"{Guid.NewGuid()}.pfx");
EnsureDevelopmentCertificates();
_output = output; _output = output;
_httpClient = new HttpClient(new HttpClientHandler() _httpClient = new HttpClient(new HttpClientHandler()
{ {
AllowAutoRedirect = true, AllowAutoRedirect = true,
UseCookies = true, UseCookies = true,
CookieContainer = new CookieContainer(), CookieContainer = new CookieContainer(),
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => (certificate.Subject != "CN=localhost" && errors == SslPolicyErrors.None) || certificate?.Thumbprint == _certificateThumbprint,
}) })
{ {
Timeout = TimeSpan.FromMinutes(2) Timeout = TimeSpan.FromMinutes(2)
}; };
_certificatePath = Path.Combine(workingDirectory, $"{Guid.NewGuid()}.pfx");
EnsureDevelopmentCertificates();
output.WriteLine("Running ASP.NET application..."); output.WriteLine("Running ASP.NET application...");
var arguments = published ? $"exec {dllPath}" : "run"; var arguments = published ? $"exec {dllPath}" : "run";
@ -70,8 +71,8 @@ namespace Templates.Test.Helpers
var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables) var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables)
{ {
["ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PATH"] = _certificatePath, ["ASPNETCORE_Kestrel__Certificates__Default__Path"] = _certificatePath,
["ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PASSWORD"] = _certificatePassword ["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _certificatePassword
}; };
Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables); Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables);
@ -81,8 +82,8 @@ namespace Templates.Test.Helpers
if (hasListeningUri) if (hasListeningUri)
{ {
logger?.LogInformation("AspNetProcess - Getting listening uri"); logger?.LogInformation("AspNetProcess - Getting listening uri");
ListeningUri = GetListeningUri(output) ?? throw new InvalidOperationException("Couldn't find the listening URL."); ListeningUri = ResolveListeningUrl(output);
logger?.LogInformation($"AspNetProcess - Got {ListeningUri.ToString()}"); logger?.LogInformation($"AspNetProcess - Got {ListeningUri}");
} }
} }
@ -91,6 +92,7 @@ namespace Templates.Test.Helpers
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
var manager = CertificateManager.Instance; var manager = CertificateManager.Instance;
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1)); var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
_certificateThumbprint = certificate.Thumbprint;
manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword); manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword);
} }
@ -134,13 +136,13 @@ namespace Templates.Test.Helpers
public async Task ContainsLinks(Page page) public async Task ContainsLinks(Page page)
{ {
var response = await RequestWithRetries(client => var response = await RetryHelper.RetryRequest(async () =>
{ {
var request = new HttpRequestMessage( var request = new HttpRequestMessage(
HttpMethod.Get, HttpMethod.Get,
new Uri(ListeningUri, page.Url)); new Uri(ListeningUri, page.Url));
return client.SendAsync(request); return await _httpClient.SendAsync(request);
}, _httpClient); }, logger: NullLogger.Instance);
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var parser = new HtmlParser(); var parser = new HtmlParser();
@ -173,7 +175,7 @@ namespace Templates.Test.Helpers
Assert.True(string.Equals(anchor.Href, expectedLink), $"Expected next link to be {expectedLink} but it was {anchor.Href}."); Assert.True(string.Equals(anchor.Href, expectedLink), $"Expected next link to be {expectedLink} but it was {anchor.Href}.");
var result = await RetryHelper.RetryRequest(async () => var result = await RetryHelper.RetryRequest(async () =>
{ {
return await RequestWithRetries(client => client.GetAsync(anchor.Href), _httpClient); return await _httpClient.GetAsync(anchor.Href);
}, logger: NullLogger.Instance); }, logger: NullLogger.Instance);
Assert.True(IsSuccessStatusCode(result), $"{anchor.Href} is a broken link!"); Assert.True(IsSuccessStatusCode(result), $"{anchor.Href} is a broken link!");
@ -203,7 +205,7 @@ namespace Templates.Test.Helpers
throw new InvalidOperationException("Max retries reached."); throw new InvalidOperationException("Max retries reached.");
} }
private Uri GetListeningUri(ITestOutputHelper output) private Uri ResolveListeningUrl(ITestOutputHelper output)
{ {
// Wait until the app is accepting HTTP requests // Wait until the app is accepting HTTP requests
output.WriteLine("Waiting until ASP.NET application is accepting connections..."); output.WriteLine("Waiting until ASP.NET application is accepting connections...");
@ -232,21 +234,27 @@ namespace Templates.Test.Helpers
private string GetListeningMessage() private string GetListeningMessage()
{ {
var buffer = new List<string>();
try try
{ {
return Process foreach (var line in Process.OutputLinesAsEnumerable)
// This will timeout at most after 5 minutes. {
.OutputLinesAsEnumerable if (line != null)
.Where(line => line != null) {
// This used to do StartsWith, but this is less strict and can prevent issues (very rare) where buffer.Add(line);
// console logging interleaves with other console output in a bad way. For example: if (line.Trim().Contains(ListeningMessagePrefix, StringComparison.Ordinal))
// dbugNow listening on: http://127.0.0.1:12857 {
.FirstOrDefault(line => line.Trim().Contains(ListeningMessagePrefix, StringComparison.Ordinal)); return line;
}
}
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return null;
} }
throw new InvalidOperationException(@$"Couldn't find listening url:
{string.Join(Environment.NewLine, buffer)}");
} }
private bool IsSuccessStatusCode(HttpResponseMessage response) private bool IsSuccessStatusCode(HttpResponseMessage response)
@ -260,14 +268,13 @@ namespace Templates.Test.Helpers
public Task AssertNotFound(string requestUrl) public Task AssertNotFound(string requestUrl)
=> AssertStatusCode(requestUrl, HttpStatusCode.NotFound); => AssertStatusCode(requestUrl, HttpStatusCode.NotFound);
internal Task<HttpResponseMessage> SendRequest(string path) internal Task<HttpResponseMessage> SendRequest(string path) =>
{ RetryHelper.RetryRequest(() => _httpClient.GetAsync(new Uri(ListeningUri, path)), logger: NullLogger.Instance);
return RequestWithRetries(client => client.GetAsync(new Uri(ListeningUri, path)), _httpClient);
}
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null) public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
{ {
var response = await RequestWithRetries(client => { var response = await RetryHelper.RetryRequest(() =>
{
var request = new HttpRequestMessage( var request = new HttpRequestMessage(
HttpMethod.Get, HttpMethod.Get,
new Uri(ListeningUri, requestUrl)); new Uri(ListeningUri, requestUrl));
@ -277,8 +284,9 @@ namespace Templates.Test.Helpers
request.Headers.Add("Accept", acceptContentType); request.Headers.Add("Accept", acceptContentType);
} }
return client.SendAsync(request); return _httpClient.SendAsync(request);
}, _httpClient); }, logger: NullLogger.Instance);
Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'."); Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'.");
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- Shared testing infrastructure for running E2E tests using selenium --> <!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" /> <Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" />
@ -28,6 +28,7 @@
<EmbeddedResource Include="template-baselines.json" /> <EmbeddedResource Include="template-baselines.json" />
<Compile Include="$(SharedSourceRoot)Process\*.cs" LinkBase="shared\Process" /> <Compile Include="$(SharedSourceRoot)Process\*.cs" LinkBase="shared\Process" />
<Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" /> <Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" />
<Compile Include="..\Shared\**" LinkBase="Helpers" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>