diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs index 23f0cc7c37f..42926643e45 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs @@ -2,11 +2,12 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Management.Automation; -using System.Management.Automation.Internal; - -using Newtonsoft.Json; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Security; using Newtonsoft.Json.Linq; using NJsonSchema; @@ -15,50 +16,122 @@ namespace Microsoft.PowerShell.Commands /// /// This class implements Test-Json command. /// - [Cmdlet(VerbsDiagnostic.Test, "Json", HelpUri = "")] + [Cmdlet(VerbsDiagnostic.Test, "Json", DefaultParameterSetName = ParameterAttribute.AllParameterSets, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096609")] public class TestJsonCommand : PSCmdlet { + private const string SchemaFileParameterSet = "SchemaFile"; + private const string SchemaStringParameterSet = "SchemaString"; + /// - /// An JSON to be validated. + /// Gets or sets JSON string to be validated. /// [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] public string Json { get; set; } /// - /// A schema to validate the JSON against. + /// Gets or sets schema to validate the JSON against. /// This is optional parameter. /// If the parameter is absent the cmdlet only attempts to parse the JSON string. /// If the parameter present the cmdlet attempts to parse the JSON string and /// then validates the JSON against the schema. Before testing the JSON string, /// the cmdlet parses the schema doing implicitly check the schema too. /// - [Parameter(Position = 1)] - [ValidateNotNullOrEmpty()] + [Parameter(Position = 1, ParameterSetName = SchemaStringParameterSet)] + [ValidateNotNullOrEmpty] public string Schema { get; set; } + /// + /// Gets or sets path to the file containg schema to validate the JSON string against. + /// This is optional parameter. + /// + [Parameter(Position = 1, ParameterSetName = SchemaFileParameterSet)] + [ValidateNotNullOrEmpty] + public string SchemaFile { get; set; } + private JsonSchema _jschema; /// - /// Prepare an JSON schema. + /// Process all exceptions in the AggregateException. + /// Unwrap TargetInvocationException if any and + /// rethrow inner exception without losing the stack trace. + /// + /// AggregateException to be unwrapped. + /// Return value is unreachable since we always rethrow. + private static bool UnwrapException(Exception e) + { + if (e is TargetInvocationException) + { + ExceptionDispatchInfo.Capture(e.InnerException).Throw(); + } + else + { + ExceptionDispatchInfo.Capture(e).Throw(); + } + + return true; + } + + /// + /// Prepare a JSON schema. /// protected override void BeginProcessing() { - if (Schema != null) + string resolvedpath = string.Empty; + + try { - try + if (Schema != null) { - _jschema = JsonSchema.FromJsonAsync(Schema).Result; + try + { + _jschema = JsonSchema.FromJsonAsync(Schema).Result; + } + catch (AggregateException ae) + { + // Even if only one exception is thrown, it is still wrapped in an AggregateException exception + // https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library + ae.Handle(UnwrapException); + } } - catch (Exception exc) + else if (SchemaFile != null) { - Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonSchema, exc); - ThrowTerminatingError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, null)); + try + { + resolvedpath = Context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(SchemaFile); + _jschema = JsonSchema.FromFileAsync(resolvedpath).Result; + } + catch (AggregateException ae) + { + ae.Handle(UnwrapException); + } } } + catch (Exception e) when ( + // Handle exceptions related to file access to provide more specific error message + // https://docs.microsoft.com/en-us/dotnet/standard/io/handling-io-errors + e is IOException || + e is UnauthorizedAccessException || + e is NotSupportedException || + e is SecurityException + ) + { + Exception exception = new Exception( + string.Format( + CultureInfo.CurrentUICulture, + TestJsonCmdletStrings.JsonSchemaFileOpenFailure, + resolvedpath), + e); + ThrowTerminatingError(new ErrorRecord(exception, "JsonSchemaFileOpenFailure", ErrorCategory.OpenError, resolvedpath)); + } + catch (Exception e) + { + Exception exception = new Exception(TestJsonCmdletStrings.InvalidJsonSchema, e); + ThrowTerminatingError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, resolvedpath)); + } } /// - /// Validate an JSON. + /// Validate a JSON. /// protected override void ProcessRecord() { diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx index a5c8d5d24d9..ab105e47fd3 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx @@ -126,4 +126,7 @@ The JSON is not valid with the schema. + + Can not open JSON schema file: {0} + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 index cf5a9438406..6ca6511cd86 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 @@ -3,6 +3,12 @@ Describe "Test-Json" -Tags "CI" { BeforeAll { + $validSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath valid_schema_reference.json + + $invalidSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath invalid_schema_reference.json + + $missingSchemaJsonPath = Join-Path -Path (Join-Path $PSScriptRoot -ChildPath assets) -ChildPath no_such_file.json + $validSchemaJson = @" { 'description': 'A person', @@ -61,40 +67,73 @@ Describe "Test-Json" -Tags "CI" { "@ } + It "Missing JSON schema file doesn't exist" { + Test-Path -LiteralPath $missingSchemaJsonPath | Should -BeFalse + } + It "Json is valid" { Test-Json -Json $validJson | Should -BeTrue } - It "Json is valid against a valid schema" { + It "Json is valid against a valid schema from string" { Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue } + It "Json is valid against a valid schema from file" { + Test-Json -Json $validJson -SchemaFile $validSchemaJsonPath | Should -BeTrue + } + It "Json is invalid" { Test-Json -Json $invalidNodeInJson -ErrorAction SilentlyContinue | Should -BeFalse } - It "Json is invalid against a valid schema" { + It "Json is invalid against a valid schema from string" { Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse Test-Json -Json $invalidNodeInJson -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse } - It "Test-Json throw if a schema is invalid" { + It "Json is invalid against a valid schema from file" { + Test-Json -Json $invalidTypeInJson2 -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + Test-Json -Json $invalidNodeInJson -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + } + + It "Test-Json throw if a schema from string is invalid" { { Test-Json -Json $validJson -Schema $invalidSchemaJson -ErrorAction Stop } | Should -Throw -ErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } - It "Test-Json write an error on invalid () Json against a valid schema" -TestCases @( + It "Test-Json throw if a schema from file is invalid" { + { Test-Json -Json $validJson -SchemaFile $invalidSchemaJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + } + + It "Test-Json throw if a path to a schema from file is invalid" { + { Test-Json -Json $validJson -SchemaFile $missingSchemaJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "JsonSchemaFileOpenFailure,Microsoft.PowerShell.Commands.TestJsonCommand" + } + + It "Test-Json write an error on invalid () Json against a valid schema from string" -TestCases @( @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } @{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } - ) { - param ($json, $errorId) + ) { + param ($json, $errorId) - $errorVar = $null - Test-Json -Json $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue + $errorVar = $null + Test-Json -Json $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue - $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId + $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId } - It "Test-Json return all errors when check invalid Json against a valid schema" { + It "Test-Json write an error on invalid () Json against a valid schema from file" -TestCases @( + @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } + ) { + param ($json, $errorId) + + $errorVar = $null + Test-Json -Json $json -SchemaFile $validSchemaJsonPath -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId + } + + It "Test-Json return all errors when check invalid Json against a valid schema from string" { $errorVar = $null Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue @@ -103,4 +142,14 @@ Describe "Test-Json" -Tags "CI" { $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + + It "Test-Json return all errors when check invalid Json against a valid schema from file" { + $errorVar = $null + Test-Json -Json $invalidTypeInJson2 -SchemaFile $validSchemaJsonPath -ErrorVariable errorVar -ErrorAction SilentlyContinue + + # '$invalidTypeInJson2' contains two errors in property types. + $errorVar.Count | Should -Be 2 + $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json new file mode 100644 index 00000000000..d3fc0cdeef9 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_definitions.json @@ -0,0 +1,8 @@ +{ + "definitions": { + "name": { + "type": "string" + }, + "hobbies" + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json new file mode 100644 index 00000000000..32520f59496 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/invalid_schema_reference.json @@ -0,0 +1,12 @@ +{ + "description": "A person", + "type": "object", + "properties": { + "name": { + "$ref": "invalid_schema_definitions.json#/definitions/name" + }, + "hobbies": { + "$ref": "invalid_schema_definitions.json#/definitions/hobbies" + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json new file mode 100644 index 00000000000..5396927a5bf --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_definitions.json @@ -0,0 +1,13 @@ +{ + "definitions": { + "name": { + "type": "string" + }, + "hobbies": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json new file mode 100644 index 00000000000..aa9c18a30c7 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/valid_schema_reference.json @@ -0,0 +1,12 @@ +{ + "description": "A person", + "type": "object", + "properties": { + "name": { + "$ref": "valid_schema_definitions.json#/definitions/name" + }, + "hobbies": { + "$ref": "valid_schema_definitions.json#/definitions/hobbies" + } + } +}