diff --git a/build.ps1 b/build.ps1 index 053782d..31e50fa 100644 --- a/build.ps1 +++ b/build.ps1 @@ -28,7 +28,8 @@ Do not uninstall the application after steps are run .PARAMETER test - Run the functional tests +.PARAMETER testFilter + Run the functional tests, use testFilter to filter to tests to be performed .PARAMETER testPort The port to use for service @@ -66,6 +67,9 @@ param( [switch] $test, + [string] + $testFilter, + [int] $testPort = 55539, @@ -193,7 +197,13 @@ function CleanUp() { function StartTest() { Write-Host "$(BuildHeader) Functional tests..." - dotnet test ([System.IO.Path]::Combine($projectRoot, "test", "Microsoft.IIS.Administration.Tests", "Microsoft.IIS.Administration.Tests.csproj")) + $testProj = [System.IO.Path]::Combine($projectRoot, "test", "Microsoft.IIS.Administration.Tests", "Microsoft.IIS.Administration.Tests.csproj") + + if ($testFilter) { + dotnet test $testProj --filter $testFilter + } else { + dotnet test $testProj + } } function VerifyPath($path) { @@ -255,6 +265,9 @@ $scriptDir = Join-Path $projectRoot "scripts" # publish script only takes full path $publishPath = Join-Path $projectRoot "dist" $serviceName = GetGlobalVariable DEFAULT_SERVICE_NAME +if ($testFilter) { + $test = $true +} Write-Host "$(BuildHeader) Starting clean up..." CleanUp diff --git a/src/Microsoft.IIS.Administration.Certificates/Certificate.cs b/src/Microsoft.IIS.Administration.Certificates/Certificate.cs index be09a2f..ba12235 100644 --- a/src/Microsoft.IIS.Administration.Certificates/Certificate.cs +++ b/src/Microsoft.IIS.Administration.Certificates/Certificate.cs @@ -6,7 +6,6 @@ namespace Microsoft.IIS.Administration.Certificates { using System; using System.Collections.Generic; - using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; diff --git a/src/Microsoft.IIS.Administration.WebServer/Exceptions/DismException.cs b/src/Microsoft.IIS.Administration.WebServer/Exceptions/DismException.cs index f2ebb95..52e48b6 100644 --- a/src/Microsoft.IIS.Administration.WebServer/Exceptions/DismException.cs +++ b/src/Microsoft.IIS.Administration.WebServer/Exceptions/DismException.cs @@ -11,16 +11,20 @@ namespace Microsoft.IIS.Administration.WebServer { private int _exitCode; private string _featureName; + private string _errors; + private string _outputs; - public DismException(int exitCode, string featureName) : base() + public DismException(int exitCode, string featureName, string errors, string outputs) : base() { _exitCode = exitCode; _featureName = featureName; + _errors = errors; + _outputs = outputs; } public dynamic GetApiError() { - return ErrorHelper.DismError(_exitCode, _featureName); + return ErrorHelper.DismError(_exitCode, _featureName, _errors, _outputs); } } } diff --git a/src/Microsoft.IIS.Administration.WebServer/Services/WebServerFeatureManager.cs b/src/Microsoft.IIS.Administration.WebServer/Services/WebServerFeatureManager.cs index 1186bd4..cf98c39 100644 --- a/src/Microsoft.IIS.Administration.WebServer/Services/WebServerFeatureManager.cs +++ b/src/Microsoft.IIS.Administration.WebServer/Services/WebServerFeatureManager.cs @@ -5,6 +5,7 @@ namespace Microsoft.IIS.Administration.WebServer { using System.Diagnostics; + using System.Text; using System.Threading.Tasks; class WebServerFeatureManager : IWebServerFeatureManager @@ -36,10 +37,24 @@ namespace Microsoft.IIS.Administration.WebServer }; var tcs = new TaskCompletionSource(); - + var errorStream = new StringBuilder(); + var outputStream = new StringBuilder(); + p.ErrorDataReceived += (sender, e) => + { + errorStream.AppendLine(e.Data); + }; + p.OutputDataReceived += (sender, e) => + { + outputStream.AppendLine(e.Data); + }; p.Exited += (sender, args) => { if (p.ExitCode != 0) { - tcs.SetException(new DismException(p.ExitCode, string.Join(", ", features))); + // 3010 status code: https://github.com/microsoft/IIS.Administration/issues/236 + tcs.SetException(new DismException( + p.ExitCode, + string.Join(", ", features), + errorStream.ToString(), + outputStream.ToString())); } else { tcs.SetResult(p.ExitCode); diff --git a/src/Microsoft.IIS.Administration.WebServer/Utils/ErrorHelper.cs b/src/Microsoft.IIS.Administration.WebServer/Utils/ErrorHelper.cs index 667b1a0..5a4dc5b 100644 --- a/src/Microsoft.IIS.Administration.WebServer/Utils/ErrorHelper.cs +++ b/src/Microsoft.IIS.Administration.WebServer/Utils/ErrorHelper.cs @@ -46,11 +46,11 @@ namespace Microsoft.IIS.Administration.WebServer { }; } - public static dynamic DismError(int exitCode, string featureName) + public static dynamic DismError(int exitCode, string featureName, string errors, string outputs) { return new { title = "Server Error", - detail = "Dism Error", + detail = $"Dism Outputs: {outputs}\n\nError: {errors}", feature = featureName, exit_code = exitCode.ToString("X"), status = (int)HttpStatusCode.InternalServerError diff --git a/test/Microsoft.IIS.Administration.Tests/CentralCertificates.cs b/test/Microsoft.IIS.Administration.Tests/CentralCertificates.cs index d700f36..79984ec 100644 --- a/test/Microsoft.IIS.Administration.Tests/CentralCertificates.cs +++ b/test/Microsoft.IIS.Administration.Tests/CentralCertificates.cs @@ -18,6 +18,7 @@ namespace Microsoft.IIS.Administration.Tests using System.Diagnostics; using System.Threading.Tasks; + // NOTE: This test intermittently fails because it tries to disable/enable Windows Features. Details: https://github.com/Microsoft/IIS.Administration/issues/236 public class CentralCertificates { private static readonly string CERTIFICATES_API_PATH = $"{Configuration.Instance().TEST_SERVER_URL}/api/certificates"; @@ -40,7 +41,7 @@ namespace Microsoft.IIS.Administration.Tests } } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public async Task CanEnable() { RequireCcsTestInfrastructure(); @@ -50,7 +51,7 @@ namespace Microsoft.IIS.Administration.Tests Assert.True(Enable(FOLDER_PATH, user.Username, user.Password, PVK_PASS)); } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public async Task PathMustBeAllowed() { RequireCcsTestInfrastructure(); @@ -79,7 +80,7 @@ namespace Microsoft.IIS.Administration.Tests } } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public void CredentialsMustBeValid() { RequireCcsTestInfrastructure(); @@ -107,7 +108,7 @@ namespace Microsoft.IIS.Administration.Tests } } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public async Task DynamicallyAddsToStores() { RequireCcsTestInfrastructure(); @@ -121,7 +122,7 @@ namespace Microsoft.IIS.Administration.Tests Assert.False(GetStores().Any(store => store.Value("name").Equals(NAME, StringComparison.OrdinalIgnoreCase))); } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public async Task CcsCertificatesShown() { RequireCcsTestInfrastructure(); @@ -134,7 +135,7 @@ namespace Microsoft.IIS.Administration.Tests })); } - [Fact(Skip = "Pending https://github.com/Microsoft/IIS.Administration/issues/236")] + [Fact] public async Task CanCreateCcsBinding() { RequireCcsTestInfrastructure(); diff --git a/test/Microsoft.IIS.Administration.Tests/HttpClientExtensions.cs b/test/Microsoft.IIS.Administration.Tests/HttpClientExtensions.cs index 3c6c9d6..e10df79 100644 --- a/test/Microsoft.IIS.Administration.Tests/HttpClientExtensions.cs +++ b/test/Microsoft.IIS.Administration.Tests/HttpClientExtensions.cs @@ -4,8 +4,10 @@ namespace Microsoft.IIS.Administration.Tests { + using Microsoft.IIS.Administration.Tests.Asserts; using Newtonsoft.Json; using Newtonsoft.Json.Linq; + using System; using System.Net.Http; using System.Text; @@ -18,6 +20,19 @@ namespace Microsoft.IIS.Administration.Tests return Globals.Success(responseMessage); } + public static string AssertGet(this HttpClient client, string uri) + { + return AssertGet(client, uri, HttpAssertions.Success); + } + + public static string AssertGet(this HttpClient client, string uri, Action assert) + { + var responseMessage = client.GetAsync(uri).Result; + assert(responseMessage); + var result = responseMessage.Content.ReadAsStringAsync().Result; + return result; + } + public static JObject Get(this HttpClient client, string uri) { string result = null; @@ -79,6 +94,25 @@ namespace Microsoft.IIS.Administration.Tests return Globals.Success(response); } + public static string AssertPatch( + this HttpClient client, + string uri, + string body) + { + return AssertPatch(client, uri, body, HttpAssertions.Success); + } + + public static string AssertPatch( + this HttpClient client, + string uri, + string body, + Action assert) + { + var response = PatchRaw(client, uri, body); + assert(response); + return response.Content.ReadAsStringAsync().Result; + } + public static HttpResponseMessage PatchRaw(this HttpClient client, string uri, string body) { HttpContent content = new StringContent(body, Encoding.UTF8, "application/json"); diff --git a/test/Microsoft.IIS.Administration.Tests/Sites.cs b/test/Microsoft.IIS.Administration.Tests/Sites.cs index b90cba5..af4a3eb 100644 --- a/test/Microsoft.IIS.Administration.Tests/Sites.cs +++ b/test/Microsoft.IIS.Administration.Tests/Sites.cs @@ -105,13 +105,9 @@ namespace Microsoft.IIS.Administration.Tests certificate = GetCertificate(client) })); - string result; string body = JsonConvert.SerializeObject(site); - - Assert.True(client.Patch(Utils.Self(site), body, out result)); - + var result = client.AssertPatch(Utils.Self(site), body); JObject newSite = JsonConvert.DeserializeObject(result); - WaitForStatus(client, ref newSite); Assert.True(Utils.JEquals(site, newSite, "server_auto_start")); @@ -555,17 +551,22 @@ namespace Microsoft.IIS.Administration.Tests } } - private static JObject GetCertificate(HttpClient client) + private JObject GetCertificate(HttpClient client) { - string result; - if (!client.Get(CertificatesUrl + $"?intended_purpose={OIDServerAuth}", out result)) { - return null; - } - + string result = client.AssertGet(CertificatesUrl + $"?intended_purpose={OIDServerAuth}"); var certsObj = JObject.Parse(result); - var cert = certsObj.Value("certificates").FirstOrDefault(); - - return cert != null ? cert.ToObject() : null; + var allCerts = certsObj.Value("certificates"); + Assert.NotEmpty(allCerts); + var localCerts = allCerts.Where(c => c["subject"].Value() == "CN=localhost"); + Assert.NotEmpty(localCerts); + var defaultCertName = "Microsoft IIS Administration Server Certificate"; + var cert = localCerts.FirstOrDefault(c => c["alias"].Value() == defaultCertName); + if (cert == null) + { + cert = localCerts.First(); + _output.WriteLine($"[WARNING]: unable to find {defaultCertName}, using {cert["alias"].Value()} for tests instead."); + } + return cert.ToObject(); } } } diff --git a/test/Microsoft.IIS.Administration.Tests/asserts/Assertions.cs b/test/Microsoft.IIS.Administration.Tests/asserts/Assertions.cs new file mode 100644 index 0000000..4d469dd --- /dev/null +++ b/test/Microsoft.IIS.Administration.Tests/asserts/Assertions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.IIS.Administration.Tests.Asserts { + public static class Assertions { + public static Action All(params Action[] conditions) { + return (T v) => { + foreach (var c in conditions) { + c(v); + } + }; + } + } +} diff --git a/test/Microsoft.IIS.Administration.Tests/asserts/HttpAssertions.cs b/test/Microsoft.IIS.Administration.Tests/asserts/HttpAssertions.cs new file mode 100644 index 0000000..3ce6727 --- /dev/null +++ b/test/Microsoft.IIS.Administration.Tests/asserts/HttpAssertions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Net.Http; +using Xunit; + +namespace Microsoft.IIS.Administration.Tests.Asserts { + public static class HttpAssertions + { + public static Action Success + = (HttpResponseMessage msg) => { + Assert.True((int)msg.StatusCode >= 200, $"{Format(msg)}"); + Assert.True((int)msg.StatusCode < 300, $"{Format(msg)}"); + }; + + private static string Format(HttpResponseMessage msg) + { + try + { + string content = msg.Content.ReadAsStringAsync().Result; + return msg.ToString() + "\nContent:" + content; + } catch (Exception e) + { + return msg.ToString() + "\nError parsing content:" + e.ToString(); + } + } + } +}