Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3a6f1fd
Add SchemaPath parameter
beatcracker Feb 23, 2020
3d5e834
Remove unnecessary usings
beatcracker Feb 23, 2020
576866b
Add tests
beatcracker Feb 24, 2020
0b27f48
Add newline at the EOF
beatcracker Feb 24, 2020
f07bec0
Use named argument for 'ResolveFilePath'
beatcracker Feb 24, 2020
8b4f4d5
Remove "ReportFileOpenFailure" usage
beatcracker Feb 26, 2020
1e991d2
Handle AggregateException from async methods
beatcracker Mar 1, 2020
5a183bc
Add tests for JSON schema file open failure
beatcracker Mar 1, 2020
77a24af
Replace "An JSON" with "A JSON"
beatcracker Mar 1, 2020
c86c47d
Replace "A JSON" with "A JSON string"
beatcracker Mar 1, 2020
5b5ef80
Replace "SchemaPath" with "SchemaFile"
beatcracker Mar 2, 2020
11a2d4f
Rename inner exception variable: "i" -> "ie"
beatcracker Mar 2, 2020
0899edb
Convert delegates to private method
beatcracker Mar 3, 2020
7b064f7
Remove top-level declaration of the "resolvedpath"
beatcracker Mar 3, 2020
34f8473
Sort usings
beatcracker Mar 3, 2020
f002d31
Remove class name in parameter definition
beatcracker Mar 17, 2020
de7f360
Simplify comment
beatcracker Mar 17, 2020
046dc27
Simplify commment
beatcracker Mar 17, 2020
486bdaf
Remove comments
beatcracker Mar 17, 2020
3f17982
Add resource string for "JsonSchemaFileOpenFailure"
beatcracker Apr 13, 2020
28e8c27
Change resource string wording to include filename
beatcracker Apr 14, 2020
8116c23
Add file path to the "JsonSchemaFileOpenFailure" error message
beatcracker Apr 14, 2020
80a2671
Add schema path as a targetObject to the ErrorRecord
beatcracker Apr 16, 2020
6082522
Fix Codefactor warnings
beatcracker Apr 18, 2020
735683c
Fix Codefactor warnings
beatcracker Apr 18, 2020
a724c7c
Fix Codefactor warnings
beatcracker Apr 18, 2020
80e0006
Fix SA1623 for CodeFactor
beatcracker May 1, 2020
8429e2f
Add HelpUri
beatcracker May 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,50 +16,122 @@ namespace Microsoft.PowerShell.Commands
/// <summary>
/// This class implements Test-Json command.
/// </summary>
[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";

/// <summary>
/// An JSON to be validated.
/// Gets or sets JSON string to be validated.
/// </summary>
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
public string Json { get; set; }

/// <summary>
/// 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.
/// </summary>
[Parameter(Position = 1)]
[ValidateNotNullOrEmpty()]
[Parameter(Position = 1, ParameterSetName = SchemaStringParameterSet)]
[ValidateNotNullOrEmpty]
public string Schema { get; set; }

/// <summary>
/// Gets or sets path to the file containg schema to validate the JSON string against.
/// This is optional parameter.
/// </summary>
[Parameter(Position = 1, ParameterSetName = SchemaFileParameterSet)]
[ValidateNotNullOrEmpty]
public string SchemaFile { get; set; }

private JsonSchema _jschema;

/// <summary>
/// Prepare an JSON schema.
/// Process all exceptions in the AggregateException.
/// Unwrap TargetInvocationException if any and
/// rethrow inner exception without losing the stack trace.
/// </summary>
/// <param name="e">AggregateException to be unwrapped.</param>
/// <returns>Return value is unreachable since we always rethrow.</returns>
private static bool UnwrapException(Exception e)
{
if (e is TargetInvocationException)
{
ExceptionDispatchInfo.Capture(e.InnerException).Throw();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could avoid re-throw and write an error here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed below, if schema is invalid (or not found) shouldn't we treat it as an invalid input and throw terminating error?

E.g.: Test-Connection will throw if it can't resolve host, Test-ModuleManifest throws if path is invalid or not pointing to the psd1 file..

}
else
{
ExceptionDispatchInfo.Capture(e).Throw();
}

return true;
}

/// <summary>
/// Prepare a JSON schema.
/// </summary>
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));
}
}

/// <summary>
/// Validate an JSON.
/// Validate a JSON.
/// </summary>
protected override void ProcessRecord()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,7 @@
<data name="InvalidJsonAgainstSchema" xml:space="preserve">
<value>The JSON is not valid with the schema.</value>
</data>
<data name="JsonSchemaFileOpenFailure" xml:space="preserve">
<value>Can not open JSON schema file: {0}</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 (<name>) 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 (<name>) 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 (<name>) 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

Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"definitions": {
"name": {
"type": "string"
},
"hobbies"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"definitions": {
"name": {
"type": "string"
},
"hobbies": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}