Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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.4.2" />
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,55 @@
// ------------------------------------------------------------------------------
// 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();
}
}
}