diff --git a/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.cs b/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.cs new file mode 100644 index 000000000000..a5c6500ccf55 --- /dev/null +++ b/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.Commands.Profile.Test +{ + public class ErrorResolutionScenarioTests : AccountsTestRunner + { + public ErrorResolutionScenarioTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void WriteInformationInResolveError() + { + TestRunner.RunTestScript("Test-WriteInformationInResolveError"); + } + } +} \ No newline at end of file diff --git a/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.ps1 b/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.ps1 new file mode 100644 index 000000000000..e4eac9538033 --- /dev/null +++ b/src/Accounts/Accounts.Test/ErrorResolutionScenarioTests.ps1 @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------------- +# +# Copyright Microsoft Corporation +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------------- + +<# +.SYNOPSIS +Tests cmdlets surrounding default resource group +#> + +function Test-WriteInformationInResolveError +{ + Assert-Throws { Connect-AzAccount -Wrong } + $err = Resolve-AzError -Last + Assert-AreEqual 1 $err.length + Assert-AreEqual "Microsoft.Azure.Commands.Profile.Errors.AzureExceptionRecord" $err[0].GetType().FullName +} diff --git a/src/Accounts/Accounts.Test/ErrorResolutionTests.cs b/src/Accounts/Accounts.Test/ErrorResolutionTests.cs index 969c395ac6eb..7038b32b24a9 100644 --- a/src/Accounts/Accounts.Test/ErrorResolutionTests.cs +++ b/src/Accounts/Accounts.Test/ErrorResolutionTests.cs @@ -13,6 +13,7 @@ // ---------------------------------------------------------------------------------- using Hyak.Common; +using Microsoft.Azure.Commands.Profile.Common; using Microsoft.Azure.Commands.Profile.Errors; using Microsoft.Azure.Commands.ScenarioTest; using Microsoft.WindowsAzure.Commands.Common.Test.Mocks; @@ -29,6 +30,20 @@ namespace Microsoft.Azure.Commands.Profile.Test { public class ErrorResolutionTests { + [Cmdlet("Mock", "Resolve" + ResourceManager.Common.AzureRMConstants.AzureRMPrefix + "Error", DefaultParameterSetName = ResolveError.AnyErrorParameterSet)] + class MockResolveError : ResolveError + { + protected override void WriteInformationWrapper(string text) + { + var informationMessage = new HostInformationMessage(); + informationMessage.Message = $"{Environment.NewLine}{text}{Environment.NewLine}"; + informationMessage.NoNewLine = false; + + ICommandRuntime2 commandRuntime2 = CommandRuntime as ICommandRuntime2; + InformationRecord informationRecord = new InformationRecord(informationMessage, String.Empty); + commandRuntime2.WriteInformation(informationRecord); + } + } class TestHyakException : CloudException { public TestHyakException(string message, CloudHttpRequestErrorInfo request, CloudHttpResponseErrorInfo response) : base(message) @@ -43,12 +58,12 @@ public TestHyakException(string message, CloudHttpRequestErrorInfo request, Clou public void DoesNotThrowWithNullError() { TestExecutionHelpers.SetUpSessionAndProfile(); - var cmdlet = new ResolveError(); - var output = cmdlet.ExecuteCmdletInPipeline("Resolve-Error"); + var cmdlet = new MockResolveError(); + var output = cmdlet.ExecuteCmdletInPipeline("Mock-ResolveAzError"); Assert.True(output == null || output.Count == 0); - output = cmdlet.ExecuteCmdletInPipeline("Resolve-Error", new ErrorRecord[] { null, null }); + output = cmdlet.ExecuteCmdletInPipeline("Mock-ResolveAzError", new ErrorRecord[] { null, null }); Assert.True(output == null || output.Count == 0); - output = cmdlet.ExecuteCmdletInPipeline("Resolve-Error", new ErrorRecord[] { null, new ErrorRecord(new Exception(null), null, ErrorCategory.AuthenticationError, null) }); + output = cmdlet.ExecuteCmdletInPipeline("Mock-ResolveAzError", new ErrorRecord[] { null, new ErrorRecord(new Exception(null), null, ErrorCategory.AuthenticationError, null) }); Assert.NotNull(output); Assert.Single(output); var record = output[0] as AzureExceptionRecord; @@ -66,7 +81,7 @@ public void HandlesExceptionError() var response = new HttpResponseMessage(HttpStatusCode.BadRequest); var hyakException = new TestHyakException("exception message", CloudHttpRequestErrorInfo.Create(request), CloudHttpResponseErrorInfo.Create(response)) { - Error = new Hyak.Common.CloudError { Code="HyakCode", Message="HyakError"} + Error = new Hyak.Common.CloudError { Code = "HyakCode", Message = "HyakError" } }; var autorestException = new Microsoft.Rest.Azure.CloudException("exception message") @@ -77,9 +92,9 @@ public void HandlesExceptionError() RequestId = "AutoRestRequestId" }; - var cmdlet = new ResolveError + var cmdlet = new MockResolveError { - Error = new [] + Error = new[] { new ErrorRecord(new Exception("exception message"), "errorCode", ErrorCategory.AuthenticationError, this), new ErrorRecord(hyakException, "errorCode", ErrorCategory.ConnectionError, this), @@ -87,7 +102,6 @@ public void HandlesExceptionError() }, CommandRuntime = runtime }; - cmdlet.ExecuteCmdlet(); Assert.NotNull(runtime.OutputPipeline); Assert.Equal(3, runtime.OutputPipeline.Count); @@ -122,6 +136,11 @@ public void HandlesExceptionError() Assert.Equal("AutoRestRequestId", autorestResult.RequestId); Assert.Contains("AutorestCode", autorestResult.ServerMessage); Assert.Contains("Autorest message", autorestResult.ServerMessage); + + Assert.Single(runtime.informationStream); + char[] charsToTrim = Environment.NewLine.ToCharArray(); + Assert.Equal(AzureProfileConstants.AzurePowerShellFeedbackMessage + , runtime.informationStream[0].MessageData.ToString().Trim(charsToTrim)); } [Fact] @@ -130,10 +149,10 @@ public void HandlesNullValuesInArmExceptions() { var runtime = new MockCommandRuntime(); var hyakException = new TestHyakException(null, null, null); - + var autorestException = new Microsoft.Rest.Azure.CloudException(); - - var cmdlet = new ResolveError + + var cmdlet = new MockResolveError { Error = new[] { @@ -143,7 +162,7 @@ public void HandlesNullValuesInArmExceptions() }, CommandRuntime = runtime }; - + cmdlet.ExecuteCmdlet(); Assert.NotNull(runtime.OutputPipeline); Assert.Equal(3, runtime.OutputPipeline.Count); @@ -162,6 +181,11 @@ public void HandlesNullValuesInArmExceptions() Assert.Equal(ErrorCategory.InvalidOperation, autorestResult.ErrorCategory.Category); Assert.NotNull(autorestResult.Exception); Assert.Equal(typeof(Microsoft.Rest.Azure.CloudException), autorestResult.Exception.GetType()); + + Assert.Single(runtime.informationStream); + char[] charsToTrim = Environment.NewLine.ToCharArray(); + Assert.Equal(AzureProfileConstants.AzurePowerShellFeedbackMessage + , runtime.informationStream[0].MessageData.ToString().Trim(charsToTrim)); } [Fact] @@ -173,7 +197,7 @@ public void LastParameterFindsLastError() var cmdlet = new ResolveError { CommandRuntime = mock }; var message = "RuntimeErrorMessage"; var exception = new Exception(message); - cmdlet.ExecuteCmdletWithExceptionInPipeline("Resolve-AzureRmError", exception, new KeyValuePair("Last", null ) ); + cmdlet.ExecuteCmdletWithExceptionInPipeline("Mock-ResolveAzError", exception, new KeyValuePair("Last", null)); Assert.NotNull(mock.ErrorStream); Assert.Single(mock.ErrorStream); Assert.NotNull(mock.OutputPipeline); @@ -183,8 +207,6 @@ public void LastParameterFindsLastError() Assert.NotNull(record.Exception); Assert.Equal(typeof(Exception), record.Exception.GetType()); Assert.Equal(message, record.Message); - - } } } diff --git a/src/Accounts/Accounts.Test/SessionRecords/Microsoft.Azure.Commands.Profile.Test.ErrorResolutionScenarioTests/WriteInformationInResolveError.json b/src/Accounts/Accounts.Test/SessionRecords/Microsoft.Azure.Commands.Profile.Test.ErrorResolutionScenarioTests/WriteInformationInResolveError.json new file mode 100644 index 000000000000..8d188f863ead --- /dev/null +++ b/src/Accounts/Accounts.Test/SessionRecords/Microsoft.Azure.Commands.Profile.Test.ErrorResolutionScenarioTests/WriteInformationInResolveError.json @@ -0,0 +1,128 @@ +{ + "Entries": [ + { + "RequestUri": "/tenants?api-version=2016-06-01", + "EncodedRequestUri": "L3RlbmFudHM/YXBpLXZlcnNpb249MjAxNi0wNi0wMQ==", + "RequestMethod": "GET", + "RequestBody": "", + "RequestHeaders": { + "x-ms-client-request-id": [ + "5b28f448-239c-4ac6-ab5c-d706ee4ec51e" + ], + "Accept-Language": [ + "en-US" + ], + "User-Agent": [ + "FxVersion/4.6.28207.03", + "OSName/Windows", + "OSVersion/Microsoft.Windows.10.0.18363.", + "Microsoft.Azure.Internal.Subscriptions.SubscriptionClient/1.3.7" + ] + }, + "ResponseHeaders": { + "Cache-Control": [ + "no-cache" + ], + "Pragma": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-tenant-reads": [ + "11998" + ], + "x-ms-request-id": [ + "7762c02a-4ba5-43be-a37c-a6c802f5585f" + ], + "x-ms-correlation-request-id": [ + "7762c02a-4ba5-43be-a37c-a6c802f5585f" + ], + "x-ms-routing-request-id": [ + "EASTASIA:20200213T030718Z:7762c02a-4ba5-43be-a37c-a6c802f5585f" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Date": [ + "Thu, 13 Feb 2020 03:07:18 GMT" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Expires": [ + "-1" + ], + "Content-Length": [ + "116" + ] + }, + "ResponseBody": "{\r\n \"value\": [\r\n {\r\n \"id\": \"/tenants/54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\",\r\n \"tenantId\": \"54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"\r\n }\r\n ]\r\n}", + "StatusCode": 200 + }, + { + "RequestUri": "/subscriptions?api-version=2016-06-01", + "EncodedRequestUri": "L3N1YnNjcmlwdGlvbnM/YXBpLXZlcnNpb249MjAxNi0wNi0wMQ==", + "RequestMethod": "GET", + "RequestBody": "", + "RequestHeaders": { + "x-ms-client-request-id": [ + "b5c0f48f-43ab-4f48-a1e6-84b4a1892524" + ], + "Accept-Language": [ + "en-US" + ], + "User-Agent": [ + "FxVersion/4.6.28207.03", + "OSName/Windows", + "OSVersion/Microsoft.Windows.10.0.18363.", + "Microsoft.Azure.Internal.Subscriptions.SubscriptionClient/1.3.7" + ] + }, + "ResponseHeaders": { + "Cache-Control": [ + "no-cache" + ], + "Pragma": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-tenant-reads": [ + "11999" + ], + "x-ms-request-id": [ + "fe449bd7-d1f4-452d-9a58-84fee8efa1b7" + ], + "x-ms-correlation-request-id": [ + "fe449bd7-d1f4-452d-9a58-84fee8efa1b7" + ], + "x-ms-routing-request-id": [ + "EASTASIA:20200213T030719Z:fe449bd7-d1f4-452d-9a58-84fee8efa1b7" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Date": [ + "Thu, 13 Feb 2020 03:07:18 GMT" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Expires": [ + "-1" + ], + "Content-Length": [ + "332" + ] + }, + "ResponseBody": "{\r\n \"value\": [\r\n {\r\n \"id\": \"/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590\",\r\n \"authorizationSource\": \"RoleBased\",\r\n \"subscriptionId\": \"0b1f6471-1bf0-4dda-aec3-cb9272f09590\",\r\n \"displayName\": \"AzureSDKTest\",\r\n \"state\": \"Enabled\",\r\n \"subscriptionPolicies\": {\r\n \"locationPlacementId\": \"Internal_2014-09-01\",\r\n \"quotaId\": \"Internal_2014-09-01\",\r\n \"spendingLimit\": \"Off\"\r\n }\r\n }\r\n ]\r\n}", + "StatusCode": 200 + } + ], + "Names": {}, + "Variables": { + "SubscriptionId": "0b1f6471-1bf0-4dda-aec3-cb9272f09590" + } +} \ No newline at end of file diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 2841ecf7dba5..75ea23dbd6c7 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -21,6 +21,7 @@ ## Version 1.7.2 * Added SubscriptionId, TenantId, and execution time into data of client side telemetry +* Display Azure PowerShell survey url to Resolve-Error. ## Version 1.7.1 * Disable context auto saving when AzureRmContext.json not available diff --git a/src/Accounts/Accounts/Common/AzureProfileConstants.cs b/src/Accounts/Accounts/Common/AzureProfileConstants.cs index 06ec207a0f5a..ca65bae6a6a9 100644 --- a/src/Accounts/Accounts/Common/AzureProfileConstants.cs +++ b/src/Accounts/Accounts/Common/AzureProfileConstants.cs @@ -12,16 +12,16 @@ // limitations under the License. // ---------------------------------------------------------------------------------- -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.Azure.Commands.Profile.Properties; namespace Microsoft.Azure.Commands.Profile.Common { public static class AzureProfileConstants { public const string AzureAutosaveVariable = "Azure_Profile_Autosave"; + + public const string AzureSurveyUrl = "https://aka.ms/azpssurvey"; + + public static readonly string AzurePowerShellFeedbackMessage = string.Format(Resources.AzurePowerShellFeedback, AzureProfileConstants.AzureSurveyUrl); } } diff --git a/src/Accounts/Accounts/Errors/ResolveError.cs b/src/Accounts/Accounts/Errors/ResolveError.cs index 7045177eda13..de2639072d14 100644 --- a/src/Accounts/Accounts/Errors/ResolveError.cs +++ b/src/Accounts/Accounts/Errors/ResolveError.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Management.Automation; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Profile.Common; using System.Collections; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; @@ -79,6 +80,15 @@ public override void ExecuteCmdlet() HandleError(record); } } + WriteInformationWrapper(AzureProfileConstants.AzurePowerShellFeedbackMessage); + } + + protected virtual void WriteInformationWrapper(string text) + { + var informationMessage = new HostInformationMessage(); + informationMessage.Message = $"{Environment.NewLine}{text}{Environment.NewLine}"; + informationMessage.NoNewLine = false; + WriteInformation(informationMessage, new string[] { "PSHOST" }); } private IEnumerable GetErrorVariable() diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index 439ce8096b60..163608a2b423 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -158,6 +158,15 @@ internal static string AutosaveSettingFromSession { return ResourceManager.GetString("AutosaveSettingFromSession", resourceCulture); } } + + /// + /// Looks up a localized string similar to The Azure PowerShell team is listening, please let us know how we are doing: {0}.. + /// + internal static string AzurePowerShellFeedback { + get { + return ResourceManager.GetString("AzurePowerShellFeedback", resourceCulture); + } + } /// /// Looks up a localized string similar to Selected profile must not be null.. diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index dffd4705ec32..488f2a78e9ce 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -471,4 +471,7 @@ Fail to access profile file and will try to use process ContextAutosaveSetting mode. Detailed error: '{0}' + + The Azure PowerShell team is listening, please let us know how we are doing: {0}. + \ No newline at end of file diff --git a/tools/ScenarioTest.ResourceManager/Mocks/MockCommandRuntime.cs b/tools/ScenarioTest.ResourceManager/Mocks/MockCommandRuntime.cs index f0011975925f..d94305aa2b6e 100644 --- a/tools/ScenarioTest.ResourceManager/Mocks/MockCommandRuntime.cs +++ b/tools/ScenarioTest.ResourceManager/Mocks/MockCommandRuntime.cs @@ -23,13 +23,14 @@ namespace Microsoft.WindowsAzure.Commands.Common.Test.Mocks { - public class MockCommandRuntime : ICommandRuntime + public class MockCommandRuntime : ICommandRuntime2 { public List ErrorStream = new List(); public List OutputPipeline = new List(); public List WarningStream = new List(); public List VerboseStream = new List(); public List DebugStream = new List(); + public List informationStream = new List (); PSHost _host = new MockPSHost(); public override string ToString() @@ -100,6 +101,16 @@ public void WriteCommandDetail(string text) throw new System.NotImplementedException(); } + public bool ShouldContinue(string query, string caption, bool hasSecurityImpact, ref bool yesToAll, ref bool noToAll) + { + throw new NotImplementedException(); + } + + public void WriteInformation(InformationRecord informationRecord) + { + informationStream.Add(informationRecord); + } + public void WriteDebug(string text) { DebugStream.Add(text);