Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.Collections;
using System.Management.Automation;
using System.Net.Http;

using Microsoft.Graph.PowerShell.Authentication.Cmdlets;
using Microsoft.Graph.PowerShell.Authentication.Helpers;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using Xunit;

namespace Microsoft.Graph.Authentication.Test.Helpers
{
public class PSObjectJsonConverterTests
{
[Fact]
public void ShouldSerializePSObjectWrappedStringAsPlainString()
{
// Mimics a value produced by the PowerShell pipeline, e.g. bare $_ in ForEach-Object.
// See https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/3654.
var body = new Hashtable
{
{
"message", new Hashtable
{
{
"toRecipients", new object[]
{
new Hashtable
{
{
"emailAddress", new Hashtable
{
{ "address", PSObject.AsPSObject("recipient@example.com") }
}
}
}
}
}
}
},
{ "saveToSentItems", true }
};

var json = JsonConvert.SerializeObject(body, new PSObjectJsonConverter());

var parsedBody = JObject.Parse(json);
Assert.Equal("recipient@example.com",
parsedBody.SelectToken("message.toRecipients[0].emailAddress.address").Value<string>());
Assert.True(parsedBody.SelectToken("saveToSentItems").Value<bool>());
}

[Fact]
public void ShouldSerializePSObjectWrappedPrimitivesAsUnderlyingValues()
{
var body = new Hashtable
{
{ "count", PSObject.AsPSObject(42) },
{ "enabled", PSObject.AsPSObject(true) }
};

var json = JsonConvert.SerializeObject(body, new PSObjectJsonConverter());

var parsedBody = JObject.Parse(json);
Assert.Equal(42, parsedBody.SelectToken("count").Value<int>());
Assert.True(parsedBody.SelectToken("enabled").Value<bool>());
}

[Fact]
public void ShouldSerializePSObjectWrappedDictionary()
{
var body = new Hashtable
{
{
"emailAddress", PSObject.AsPSObject(new Hashtable
{
{ "address", PSObject.AsPSObject("recipient@example.com") }
})
}
};

var json = JsonConvert.SerializeObject(body, new PSObjectJsonConverter());

var parsedBody = JObject.Parse(json);
Assert.Equal("recipient@example.com",
parsedBody.SelectToken("emailAddress.address").Value<string>());
}

[Fact]
public void ShouldSerializePSCustomObjectAsJsonObject()
{
var emailAddress = new PSObject();
emailAddress.Properties.Add(new PSNoteProperty("address", PSObject.AsPSObject("recipient@example.com")));
var body = new Hashtable
{
{ "emailAddress", emailAddress }
};

var json = JsonConvert.SerializeObject(body, new PSObjectJsonConverter());

var parsedBody = JObject.Parse(json);
Assert.Equal("recipient@example.com",
parsedBody.SelectToken("emailAddress.address").Value<string>());
}

[Fact]
public void SetRequestContentShouldSerializeDictionaryWithPSObjectWrappedValues()
{
var cmdlet = new InvokeMgGraphRequest();
using (var request = new HttpRequestMessage(HttpMethod.Post,
"https://graph.microsoft.com/v1.0/users/sender@example.com/sendMail"))
{
var body = new Hashtable
{
{
"message", new Hashtable
{
{
"toRecipients", new object[]
{
new Hashtable
{
{
"emailAddress", new Hashtable
{
{ "address", PSObject.AsPSObject("recipient@example.com") }
}
}
}
}
}
}
},
{ "saveToSentItems", true }
};

var contentLength = cmdlet.SetRequestContent(request, body);

Assert.True(contentLength > 0);
var json = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var parsedBody = JObject.Parse(json);
Assert.Equal("recipient@example.com",
parsedBody.SelectToken("message.toRecipients[0].emailAddress.address").Value<string>());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<!-- As described in this post https://devblogs.microsoft.com/powershell/depending-on-the-right-powershell-nuget-package-in-your-net-project, reference the SDK for dotnetcore-->
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.5.0" PrivateAssets="all" Condition="'$(TargetFramework)' == 'net8.0'" />
<!-- Microsoft.PowerShell.SDK 7.5.x targets net9.0 and contributes no assemblies to a net8.0 build,
which silently leaves the PowerShellStandard.Library stub (whose APIs return null) as the runtime
System.Management.Automation. Keep this on the latest 7.4.x (net8.0) release so PSObject-dependent
tests exercise the real PowerShell runtime. -->
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.17" PrivateAssets="all" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="xunit" Version="2.9.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ private async Task<HttpResponseMessage> GetResponseAsync(HttpClient client, Http
/// <param name="request"></param>
/// <param name="content"></param>
/// <returns></returns>
private long SetRequestContent(HttpRequestMessage request, IDictionary content)
internal long SetRequestContent(HttpRequestMessage request, IDictionary content)
{
if (request == null)
{
Expand All @@ -579,8 +579,10 @@ private long SetRequestContent(HttpRequestMessage request, IDictionary content)
throw new ArgumentNullException(nameof(content));
}

// Covert all dictionaries to Json
var body = JsonConvert.SerializeObject(content);
// Convert all dictionaries to Json.
// Unwrap PSObject-wrapped values (e.g. values produced by the PowerShell pipeline) so the
// underlying CLR values are serialized instead of their PSObject wrappers. See https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/3654.
var body = JsonConvert.SerializeObject(content, new PSObjectJsonConverter());
return SetRequestContent(request, body);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

using Newtonsoft.Json;
using System;
using System.Management.Automation;

namespace Microsoft.Graph.PowerShell.Authentication.Helpers
{
/// <summary>
/// Serializes <see cref="PSObject" /> values by unwrapping them to their underlying base object.
/// PowerShell wraps values that pass through the pipeline (e.g. bare $_ in ForEach-Object) in a
/// <see cref="PSObject" />. Without this converter, Newtonsoft.Json reflects over the PowerShell
/// adapted members of the wrapper (such as the Chars indexed property on strings) instead of the
/// underlying value and fails with a self-referencing loop error.
/// </summary>
internal class PSObjectJsonConverter : JsonConverter
{
public override bool CanRead => false;

public override bool CanConvert(Type objectType)
{
return typeof(PSObject).IsAssignableFrom(objectType);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var psObject = (PSObject)value;
if (psObject.BaseObject is PSCustomObject)
{
// Pure custom objects (e.g. [pscustomobject]@{ ... }) have no underlying CLR object
// to fall back on; project their properties into a JSON object instead.
writer.WriteStartObject();
foreach (var property in psObject.Properties)
{
writer.WritePropertyName(property.Name);
serializer.Serialize(writer, property.Value);
}
writer.WriteEndObject();
}
else
{
// Serialize the underlying CLR object. Nested PSObject-wrapped values (e.g. inside
// dictionaries or collections) are routed back through this converter by the serializer.
serializer.Serialize(writer, psObject.BaseObject);
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException();
}
}
}