From 0be3eeac38e3065fb4cb7dd49d1c86aee4cb04fc Mon Sep 17 00:00:00 2001 From: Armaan Mcleod Date: Wed, 22 Feb 2023 05:14:08 +1100 Subject: [PATCH] Add `-Path` and `-LiteralPath` parameters to `Test-Json` cmdlet (#19042) --- .../commands/utility/TestJsonCommand.cs | 103 ++++++++++++- .../Test-Json.Tests.ps1 | 145 +++++++++++++++++- 2 files changed, 235 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs index 9d792b9123..fc416d9772 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs @@ -16,19 +16,63 @@ namespace Microsoft.PowerShell.Commands /// /// This class implements Test-Json command. /// - [Cmdlet(VerbsDiagnostic.Test, "Json", DefaultParameterSetName = ParameterAttribute.AllParameterSets, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096609")] + [Cmdlet(VerbsDiagnostic.Test, "Json", DefaultParameterSetName = JsonStringParameterSet, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096609")] [OutputType(typeof(bool))] public class TestJsonCommand : PSCmdlet { - private const string SchemaFileParameterSet = "SchemaFile"; - private const string SchemaStringParameterSet = "SchemaString"; + #region Parameter Set Names + + private const string JsonStringParameterSet = "JsonString"; + private const string JsonStringWithSchemaStringParameterSet = "JsonStringWithSchemaString"; + private const string JsonStringWithSchemaFileParameterSet = "JsonStringWithSchemaFile"; + private const string JsonPathParameterSet = "JsonPath"; + private const string JsonPathWithSchemaStringParameterSet = "JsonPathWithSchemaString"; + private const string JsonPathWithSchemaFileParameterSet = "JsonPathWithSchemaFile"; + private const string JsonLiteralPathParameterSet = "JsonLiteralPath"; + private const string JsonLiteralPathWithSchemaStringParameterSet = "JsonLiteralPathWithSchemaString"; + private const string JsonLiteralPathWithSchemaFileParameterSet = "JsonLiteralPathWithSchemaFile"; + + #endregion + + #region Parameters /// /// Gets or sets JSON string to be validated. /// - [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ParameterSetName = JsonStringParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ParameterSetName = JsonStringWithSchemaStringParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ParameterSetName = JsonStringWithSchemaFileParameterSet)] public string Json { get; set; } + /// + /// Gets or sets JSON file path to be validated. + /// + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonPathParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonPathWithSchemaStringParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonPathWithSchemaFileParameterSet)] + public string Path { get; set; } + + /// + /// Gets or sets JSON literal file path to be validated. + /// + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonLiteralPathParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonLiteralPathWithSchemaStringParameterSet)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = JsonLiteralPathWithSchemaFileParameterSet)] + [Alias("PSPath", "LP")] + public string LiteralPath + { + get + { + return _isLiteralPath ? Path : null; + } + + set + { + _isLiteralPath = true; + Path = value; + } + } + /// /// Gets or sets schema to validate the JSON against. /// This is optional parameter. @@ -37,7 +81,9 @@ namespace Microsoft.PowerShell.Commands /// 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, ParameterSetName = SchemaStringParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonStringWithSchemaStringParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonPathWithSchemaStringParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonLiteralPathWithSchemaStringParameterSet)] [ValidateNotNullOrEmpty] public string Schema { get; set; } @@ -45,10 +91,17 @@ namespace Microsoft.PowerShell.Commands /// Gets or sets path to the file containing schema to validate the JSON string against. /// This is optional parameter. /// - [Parameter(Position = 1, ParameterSetName = SchemaFileParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonStringWithSchemaFileParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonPathWithSchemaFileParameterSet)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = JsonLiteralPathWithSchemaFileParameterSet)] [ValidateNotNullOrEmpty] public string SchemaFile { get; set; } + #endregion + + #region Private Members + + private bool _isLiteralPath = false; private JsonSchema _jschema; /// @@ -72,6 +125,10 @@ namespace Microsoft.PowerShell.Commands return true; } + #endregion + + #region Protected Members + /// /// Prepare a JSON schema. /// @@ -137,10 +194,38 @@ namespace Microsoft.PowerShell.Commands protected override void ProcessRecord() { bool result = true; + string jsonToParse = string.Empty; + + if (Json != null) + { + jsonToParse = Json; + } + else if (Path != null) + { + string resolvedPath = PathUtils.ResolveFilePath(Path, this, _isLiteralPath); + + if (!File.Exists(resolvedPath)) + { + ItemNotFoundException exception = new( + Path, + "PathNotFound", + SessionStateStrings.PathNotFound); + + ThrowTerminatingError(exception.ErrorRecord); + } + + jsonToParse = File.ReadAllText(resolvedPath); + } + + if (string.IsNullOrWhiteSpace(jsonToParse)) + { + WriteObject(false); + return; + } try { - var parsedJson = JToken.Parse(Json); + var parsedJson = JToken.Parse(jsonToParse); if (_jschema != null) { @@ -165,10 +250,12 @@ namespace Microsoft.PowerShell.Commands result = false; Exception exception = new(TestJsonCmdletStrings.InvalidJson, exc); - WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, Json)); + WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, jsonToParse)); } WriteObject(result); } + + #endregion } } 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 2ea76f5826..9e949e0215 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 @@ -3,11 +3,11 @@ 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 + $assetsPath = Join-Path $PSScriptRoot -ChildPath assets + $validSchemaJsonPath = Join-Path -Path $assetsPath -ChildPath valid_schema_reference.json + $invalidSchemaJsonPath = Join-Path -Path $assetsPath -ChildPath invalid_schema_reference.json + $missingSchemaJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json + $missingJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json $validSchemaJson = @" { @@ -65,48 +65,139 @@ Describe "Test-Json" -Tags "CI" { errorNode } "@ + + $validJsonPath = Join-Path -Path $TestDrive -ChildPath 'validJson.json' + $validLiteralJsonPath = Join-Path -Path $TestDrive -ChildPath "[valid]Json.json" + $invalidNodeInJsonPath = Join-Path -Path $TestDrive -ChildPath 'invalidNodeInJson.json' + $invalidTypeInJsonPath = Join-Path -Path $TestDrive -ChildPath 'invalidTypeInJson.json' + $invalidTypeInJson2Path = Join-Path -Path $TestDrive -ChildPath 'invalidTypeInJson2.json' + $invalidEmptyJsonPath = Join-Path -Path $TestDrive -ChildPath 'emptyJson.json' + + Set-Content -Path $validJsonPath -Value $validJson + Set-Content -LiteralPath $validLiteralJsonPath -Value $validJson + Set-Content -Path $invalidNodeInJsonPath -Value $invalidNodeInJson + Set-Content -Path $invalidTypeInJsonPath -Value $invalidTypeInJson + Set-Content -Path $invalidTypeInJson2Path -Value $invalidTypeInJson2 + New-Item -Path $invalidEmptyJsonPath -ItemType File } It "Missing JSON schema file doesn't exist" { Test-Path -LiteralPath $missingSchemaJsonPath | Should -BeFalse } + It "Missing JSON file doesn't exist" { + Test-Path -LiteralPath $missingJsonPath | Should -BeFalse + } + It "Json is valid" { Test-Json -Json $validJson | Should -BeTrue + ($validJson | Test-Json) | Should -BeTrue } It "Json is valid against a valid schema from string" { Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue + ($validJson | Test-Json -Schema $validSchemaJson) | Should -BeTrue } It "Json is valid against a valid schema from file" { Test-Json -Json $validJson -SchemaFile $validSchemaJsonPath | Should -BeTrue + ($validJson | Test-Json -SchemaFile $validSchemaJsonPath) | Should -BeTrue + } + + It "Json file specified using -Path is valid" { + Test-Json -Path $validJsonPath | Should -BeTrue + } + + It "Json file specified using -LiteralPath is valid" { + Test-Json -LiteralPath $validLiteralJsonPath | Should -BeTrue + } + + It "Json file specified using LiteralPath aliases -PSPath and -LP is valid" { + Test-Json -PSPath $validLiteralJsonPath | Should -BeTrue + Test-Json -LP $validLiteralJsonPath | Should -BeTrue + } + + It "Json file specified using -Path from pipeline is valid" { + (Get-ChildItem -Path $validJsonPath -File | Test-Json) | Should -BeTrue + } + + It "Json file specified using -LiteralPath from pipeline is valid" { + (Get-ChildItem -LiteralPath $validLiteralJsonPath -File | Test-Json) | Should -BeTrue + } + + It "Json file is valid against a valid schema from string" { + Test-Json -Path $validJsonPath -Schema $validSchemaJson | Should -BeTrue + } + + It "Json file is valid against a valid schema from file" { + Test-Json -Path $validJsonPath -SchemaFile $validSchemaJsonPath | Should -BeTrue } It "Json is invalid" { Test-Json -Json $invalidNodeInJson -ErrorAction SilentlyContinue | Should -BeFalse + ($invalidNodeInJson | Test-Json -ErrorAction SilentlyContinue) | Should -BeFalse } It "Json is invalid against a valid schema from string" { Test-Json -Json $invalidTypeInJson2 -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse + ($invalidTypeInJson2 | Test-Json -Schema $validSchemaJson -ErrorAction SilentlyContinue) | Should -BeFalse + Test-Json -Json $invalidNodeInJson -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse + ($invalidNodeInJson | Test-Json -Schema $validSchemaJson -ErrorAction SilentlyContinue) | Should -BeFalse } It "Json is invalid against a valid schema from file" { Test-Json -Json $invalidTypeInJson2 -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + ($invalidTypeInJson2 | Test-Json -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue) | Should -BeFalse + Test-Json -Json $invalidNodeInJson -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + ($invalidNodeInJson | Test-Json -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue) | Should -BeFalse + } + + It "Json file is invalid against a valid schema from file" { + Test-Json -Path $invalidTypeInJson2Path -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + Test-Json -Path $invalidNodeInJsonPath -SchemaFile $validSchemaJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + } + + It "Json file is invalid" { + Test-Json -Path $invalidNodeInJsonPath -ErrorAction SilentlyContinue | Should -BeFalse + } + + It "Json file is invalid against a valid schema from string" { + Test-Json -Path $invalidTypeInJson2Path -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse + Test-Json -Path $invalidNodeInJsonPath -Schema $validSchemaJson -ErrorAction SilentlyContinue | Should -BeFalse + } + + It "Json file is invalid against an empty file" { + Test-Json -Path $invalidEmptyJsonPath -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" + { Test-Json -Path $validJsonPath -Schema $invalidSchemaJson -ErrorAction Stop } | Should -Throw -ErrorId "InvalidJsonSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } 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" + { Test-Json -Path $validJsonPath -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" + { Test-Json -Path $validJsonPath -SchemaFile $missingSchemaJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "JsonSchemaFileOpenFailure,Microsoft.PowerShell.Commands.TestJsonCommand" + } + + It "Test-Json throw if a path from file is invalid" { + { Test-Json -Path $missingJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.TestJsonCommand" + } + + It "Test-Json throw if a path from file using -Path is a literal path" { + { Test-Json -Path $validLiteralJsonPath -ErrorAction Stop } | Should -Throw -ErrorId "FileOpenFailure,Microsoft.PowerShell.Commands.TestJsonCommand" + } + + It "Json file throw if a path from file using -LiteralPath is a wildcard or regular expression" { + { Test-Json -LiteralPath (Join-Path -Path $TestDrive -ChildPath "*Json.json") -ErrorAction Stop } | Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.TestJsonCommand" + { Test-Json -LiteralPath (Join-Path -Path $TestDrive -ChildPath "[a-z]Json.json") -ErrorAction Stop } | Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.TestJsonCommand" } It "Test-Json write an error on invalid () Json against a valid schema from string" -TestCases @( @@ -133,6 +224,30 @@ Describe "Test-Json" -Tags "CI" { $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId } + It "Test-Json write an error on invalid () Json file against a valid schema from string" -TestCases @( + @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } + ) { + param ($json, $errorId) + + $errorVar = $null + Test-Json -Path $json -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $errorVar.FullyQualifiedErrorId | Should -BeExactly $errorId + } + + It "Test-Json write an error on invalid () Json file against a valid schema from file" -TestCases @( + @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } + ) { + param ($json, $errorId) + + $errorVar = $null + Test-Json -Path $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 @@ -153,6 +268,26 @@ Describe "Test-Json" -Tags "CI" { $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + It "Test-Json return all errors when check invalid Json file against a valid schema from string" { + $errorVar = $null + Test-Json -Path $invalidTypeInJson2Path -Schema $validSchemaJson -ErrorVariable errorVar -ErrorAction SilentlyContinue + + # '$invalidTypeInJson2Path' 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" + } + + It "Test-Json return all errors when check invalid Json file against a valid schema from file" { + $errorVar = $null + Test-Json -Path $invalidTypeInJson2Path -SchemaFile $validSchemaJsonPath -ErrorVariable errorVar -ErrorAction SilentlyContinue + + # '$invalidTypeInJson2Path' 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" + } + It 'Test-Json recognizes non-object types: ' -TestCases @( @{ name = 'number'; value = 1; expected = 'number' } @{ name = '"true"'; value = '"true"'; expected = 'string' }