[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:
parent
88dbfaa2ab
commit
d6409371ae
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue