fix udi leaking in the management api (#15684)

* [WIP] Stop Udi leaking on ConterPicker

* Refined Udi conversion for contentPicker

Cleaned up base construcor usage to move away from the obsoleted one.

* Fixed Udi lieaking in MNTP

* Stopped Udi bleeding for MultiUrlPicker

* Remove unused assignment

* Resolved namespace issue

* Use correct configuration value for MNTP udi parsing

* Turn helper auto props into local helper function to avoid unnecesary serialization

* Remove Newtonsoft.Json from Multi URL picker

* Fixed MNTP configuration serialization

* Changed MNTP editor data from csv guid to EditorEntityReference[]

* Added remarks for the MNTP editor conversion logic

* Reworked MNTPPropertyEditor Unittests

changed intent of one
fixed bug because of 1 rework

* Update OpenApi.json

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Elitsa <elm@umbraco.dk>
Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
Sven Geusens
2024-03-14 11:06:46 +01:00
committed by GitHub
parent f41a4f1f06
commit ffcc4ac170
8 changed files with 325 additions and 94 deletions

View File

@@ -22664,8 +22664,16 @@
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/security/forgot-password/reset": {
@@ -22969,16 +22977,8 @@
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
}
},
"/umbraco/management/api/v1/server/configuration": {
@@ -31027,16 +31027,8 @@
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
}
},
"/umbraco/management/api/v1/user/set-user-groups": {
@@ -31230,6 +31222,62 @@
}
},
"/umbraco/management/api/v1/webhook": {
"get": {
"tags": [
"Webhook"
],
"operationId": "GetWebhook",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PagedWebhookResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/PagedWebhookResponseModel"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PagedWebhookResponseModel"
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
},
"post": {
"tags": [
"Webhook"
@@ -34061,12 +34109,27 @@
},
"DatatypeConfigurationResponseModel": {
"required": [
"canBeChanged"
"canBeChanged",
"documentListViewId",
"mediaListViewId",
"memberListViewId"
],
"type": "object",
"properties": {
"canBeChanged": {
"$ref": "#/components/schemas/DataTypeChangeModeModel"
},
"documentListViewId": {
"type": "string",
"format": "uuid"
},
"mediaListViewId": {
"type": "string",
"format": "uuid"
},
"memberListViewId": {
"type": "string",
"format": "uuid"
}
},
"additionalProperties": false
@@ -38115,6 +38178,30 @@
},
"additionalProperties": false
},
"PagedWebhookResponseModel": {
"required": [
"items",
"total"
],
"type": "object",
"properties": {
"total": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/WebhookResponseModel"
}
]
}
}
},
"additionalProperties": false
},
"PartialViewFolderResponseModel": {
"type": "object",
"allOf": [

View File

@@ -23,9 +23,19 @@ public class LinkDisplay
[DataMember(Name = "trashed")]
public bool Trashed { get; set; }
[DataMember(Name = "udi")]
public GuidUdi? Udi { get; set; }
[DataMember(Name = "type")]
public string? Type { get; set; }
[DataMember(Name = "unique")]
public Guid? Unique { get; set; }
[DataMember(Name = "url")]
public string? Url { get; set; }
public static class Types
{
public const string Document = "document";
public const string Media = "media";
public const string External = "external";
}
}

View File

@@ -1,11 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -35,6 +39,7 @@ public class ContentPickerPropertyEditor : DataEditor
internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference
{
public ContentPickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
@@ -58,5 +63,40 @@ public class ContentPickerPropertyEditor : DataEditor
yield return new UmbracoEntityReference(udi);
}
}
// starting in v14 the passed in value is always a guid, we store it as a document Udi string. Else it's an invalid value
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) =>
editorValue.Value is not null
&& Guid.TryParse(editorValue.Value as string, out Guid guidValue)
? GuidUdi.Create(Constants.UdiEntityType.Document, guidValue).ToString()
: null;
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
// since our storage type is a string, we can expect the base to return a string
var stringValue = base.ToEditor(property, culture, segment) as string;
if (stringValue.IsNullOrWhiteSpace())
{
return null;
}
// this string can actually be an Int value from old versions => convert to it's guid counterpart
if (int.TryParse(stringValue, out var oldInt))
{
// todo: This is a temporary code path that should be removed ASAP
Attempt<Guid> conversionAttempt = StaticServiceProvider.Instance.GetRequiredService<IIdKeyMap>()
.GetKeyForId(oldInt, UmbracoObjectTypes.Document);
return conversionAttempt.Success ? conversionAttempt.Result : null;
}
// if its not an old value, it should be a udi
if (UdiParser.TryParse(stringValue, out GuidUdi? guidUdi) is false)
{
return null;
}
return guidUdi.Guid;
}
}
}

View File

@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
@@ -5,6 +7,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig
{
[JsonPropertyName("startNode")]
[ConfigurationField("startNode")]
public MultiNodePickerConfigurationTreeSource? TreeSource { get; set; }

View File

@@ -1,4 +1,5 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -8,15 +9,18 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataContract]
public class MultiNodePickerConfigurationTreeSource
{
[JsonPropertyName("type")]
[DataMember(Name = "type")]
public string? ObjectType { get; set; }
[JsonPropertyName("query")]
[DataMember(Name = "query")]
public string? StartNodeQuery { get; set; }
[DataMember(Name = "dynamicRoot")]
public DynamicRoot? DynamicRoot { get; set; }
[JsonPropertyName("id")]
[DataMember(Name = "id")]
public Udi? StartNodeId { get; set; }
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -31,8 +32,16 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<MultiNodeTreePickerPropertyValueEditor>(Attribute!);
/// <remarks>
/// At first glance, the fromEditor and toEditor methods might seem strange.
/// This is because we wanted to stop the leaking of UDI's to the frontend while not having to do database migrations
/// so we opted to, for now, translate the udi string in the database into a structured format unique to the client
/// This way, for now, no migration is needed and no changes outside of the editor logic needs to be touched to stop the leaking.
/// </remarks>
public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly IJsonSerializer _jsonSerializer;
public MultiNodeTreePickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
@@ -40,6 +49,7 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
DataEditorAttribute attribute)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
}
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
@@ -56,23 +66,41 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
}
}
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
=> editorValue.Value is JsonArray jsonArray
? EntityReferencesToUdis(_jsonSerializer.Deserialize<IEnumerable<EditorEntityReference>>(jsonArray.ToJsonString()) ?? Enumerable.Empty<EditorEntityReference>())
: null;
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
return value is string stringValue
? ParseValidUdis(stringValue.Split(Constants.CharArrays.Comma))
: null;
? UdisToEntityReferences(stringValue.Split(Constants.CharArrays.Comma)).ToArray()
: null;
}
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
=> editorValue.Value is IEnumerable<string> stringValues
? string.Join(",", ParseValidUdis(stringValues))
: null;
private IEnumerable<EditorEntityReference> UdisToEntityReferences(IEnumerable<string> stringUdis)
{
foreach (var stringUdi in stringUdis)
{
if (UdiParser.TryParse(stringUdi, out GuidUdi? guidUdi) is false)
{
continue;
}
private string[] ParseValidUdis(IEnumerable<string> stringValues)
=> stringValues
.Select(s => UdiParser.TryParse(s, out Udi? udi) && udi is GuidUdi guidUdi ? guidUdi.ToString() : null)
.WhereNotNull()
.ToArray();
yield return new EditorEntityReference() { Type = guidUdi.EntityType, Unique = guidUdi.Guid };
}
}
private string EntityReferencesToUdis(IEnumerable<EditorEntityReference> nodeReferences)
=> string.Join(",", nodeReferences.Select(entityReference => Udi.Create(entityReference.Type, entityReference.Unique).ToString()));
public class EditorEntityReference
{
public required string Type { get; set; }
public required Guid Unique { get; set; }
}
}
}

View File

@@ -130,7 +130,9 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
Trashed = trashed,
Published = published,
QueryString = dto.QueryString,
Udi = udi,
Type = dto.Udi is null ? LinkDisplay.Types.External
: dto.Udi.EntityType,
Unique = dto.Udi?.Guid,
Url = url ?? string.Empty,
});
}
@@ -169,8 +171,8 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
Name = link.Name,
QueryString = link.QueryString,
Target = link.Target,
Udi = link.Udi,
Url = link.Udi == null ? link.Url : null, // only save the URL for external links
Udi = TypeIsUdiBased(link) ? new GuidUdi(link.Type!, link.Unique!.Value) : null,
Url = TypeIsExternal(link) ? link.Url : null, // only save the URL for external links
}));
}
catch (Exception ex)
@@ -181,6 +183,14 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
return base.FromEditor(editorValue, currentValue);
}
private static bool TypeIsExternal(LinkDisplay link) =>
link.Type is not null && link.Type.Equals(LinkDisplay.Types.External, StringComparison.InvariantCultureIgnoreCase);
private static bool TypeIsUdiBased(LinkDisplay link) =>
link.Type is not null && link.Unique is not null &&
(link.Type.Equals(LinkDisplay.Types.Document, StringComparison.InvariantCultureIgnoreCase)
|| link.Type.Equals(LinkDisplay.Types.Media, StringComparison.InvariantCultureIgnoreCase));
[DataContract]
public class LinkDto
{

View File

@@ -1,13 +1,15 @@
using Moq;
using System.Text.Json.Nodes;
using Moq;
using NUnit.Framework;
using Org.BouncyCastle.Asn1.X500;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
@@ -67,9 +69,11 @@ public class MultiNodeTreePickerTests
[Test]
public void Can_Parse_Single_Value_From_Editor()
{
var value = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString();
var fromEditor = FromEditor(new[] { value }) as string;
Assert.AreEqual(value, fromEditor);
var value = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid());
var editorValue = $"[{{\"type\" :\"{value.EntityType}\",\"unique\":\"{value.Guid}\"}}]";
var fromEditor =
FromEditor(JsonNode.Parse(editorValue), jsonSerializer: new SystemTextJsonSerializer()) as string;
Assert.AreEqual(value.ToString(), fromEditor);
}
[Test]
@@ -77,51 +81,65 @@ public class MultiNodeTreePickerTests
{
var values = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString()
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid())
};
var fromEditor = FromEditor(values) as string;
Assert.AreEqual(string.Join(",", values), fromEditor);
var editorValue =
$"[{{\"type\" :\"{values[0].EntityType}\",\"unique\":\"{values[0].Guid}\"}},{{\"type\" :\"{values[1].EntityType}\",\"unique\":\"{values[1].Guid}\"}},{{\"type\" :\"{values[2].EntityType}\",\"unique\":\"{values[2].Guid}\"}}]";
var fromEditor = FromEditor(JsonNode.Parse(editorValue), jsonSerializer: new SystemTextJsonSerializer()) as string;
Assert.AreEqual(string.Join(",", values.Select(v => v.ToString())), fromEditor);
}
[Test]
public void Can_Parse_Different_Entity_Types_From_Editor()
{
var values = new[]
var expectedValues = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Member, Guid.NewGuid()).ToString()
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Media, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Member, Guid.NewGuid())
};
var fromEditor = FromEditor(values) as string;
Assert.AreEqual(string.Join(",", values), fromEditor);
var editorValue =
$"[{{\"type\" :\"{expectedValues[0].EntityType}\",\"unique\":\"{expectedValues[0].Guid}\"}},{{\"type\" :\"{expectedValues[1].EntityType}\",\"unique\":\"{expectedValues[1].Guid}\"}},{{\"type\" :\"{expectedValues[2].EntityType}\",\"unique\":\"{expectedValues[2].Guid}\"}}]";
var fromEditor = FromEditor(JsonNode.Parse(editorValue), jsonSerializer: new SystemTextJsonSerializer()) as string;
Assert.AreEqual(string.Join(",", expectedValues.Select(v => v.ToString())), fromEditor);
}
[Test]
public void Can_Skip_Invalid_Values_From_Editor()
public void From_Editor_Throws_Error_On_Invalid_Json()
{
var values = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
"Invalid Value",
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString()
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid())
};
var fromEditor = FromEditor(values) as string;
Assert.AreEqual(string.Join(",", values.First(), values.Last()), fromEditor);
var editorValue =
$"[{{\"type\" :\"{values[0].EntityType}\",\"unique\":\"{values[0].Guid}\"}},{{\"invalidProperty\" :\"nonsenseValue\",\"otherWeirdProperty\":\"definitelyNotAGuid\"}},{{\"type\" :\"{values[1].EntityType}\",\"unique\":\"{values[1].Guid}\"}}]";
Assert.Catch<System.Text.Json.JsonException>(() =>
FromEditor(JsonNode.Parse(editorValue), jsonSerializer: new SystemTextJsonSerializer()));
}
[Test]
public void Can_Parse_Single_Value_To_Editor()
{
var value = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString();
var toEditor = ToEditor(value) as IEnumerable<string>;
Assert.IsNotNull(toEditor);
Assert.AreEqual(1, toEditor.Count());
Assert.AreEqual(value, toEditor.First());
var value = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid());
var toEditor = ToEditor(value.ToString()) as IEnumerable<MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.EditorEntityReference>;
Assert.Multiple(() =>
{
Assert.IsNotNull(toEditor);
Assert.AreEqual(1, toEditor.Count());
Assert.AreEqual(EditorEntityReferenceFromUdi(value).Type, toEditor.First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(value).Unique, toEditor.First().Unique);
});
}
[Test]
@@ -129,16 +147,23 @@ public class MultiNodeTreePickerTests
{
var values = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString()
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid())
};
var toEditor = ToEditor(string.Join(",", values)) as IEnumerable<string>;
Assert.IsNotNull(toEditor);
Assert.AreEqual(3, toEditor.Count());
Assert.AreEqual(values[0], toEditor.First());
Assert.AreEqual(values[1], toEditor.Skip(1).First());
Assert.AreEqual(values[2], toEditor.Last());
var toEditor = ToEditor(string.Join(",", values.Select(v => v.ToString()))) as IEnumerable<MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.EditorEntityReference>;
Assert.Multiple(() =>
{
Assert.IsNotNull(toEditor);
Assert.AreEqual(3, toEditor.Count());
Assert.AreEqual(EditorEntityReferenceFromUdi(values[0]).Type, toEditor.First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[0]).Unique, toEditor.First().Unique);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[1]).Type, toEditor.Skip(1).First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[1]).Unique, toEditor.Skip(1).First().Unique);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[2]).Type, toEditor.Last().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[2]).Unique, toEditor.Last().Unique);
});
}
[Test]
@@ -146,32 +171,46 @@ public class MultiNodeTreePickerTests
{
var values = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(),
new GuidUdi(Constants.UdiEntityType.Member, Guid.NewGuid()).ToString()
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Media, Guid.NewGuid()),
new GuidUdi(Constants.UdiEntityType.Member, Guid.NewGuid())
};
var toEditor = ToEditor(string.Join(",", values)) as IEnumerable<string>;
Assert.IsNotNull(toEditor);
Assert.AreEqual(3, toEditor.Count());
Assert.AreEqual(values[0], toEditor.First());
Assert.AreEqual(values[1], toEditor.Skip(1).First());
Assert.AreEqual(values[2], toEditor.Last());
var toEditor = ToEditor(string.Join(",", values.Select(v => v.ToString()))) as IEnumerable<MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.EditorEntityReference>;
Assert.Multiple(() =>
{
Assert.IsNotNull(toEditor);
Assert.AreEqual(3, toEditor.Count());
Assert.AreEqual(EditorEntityReferenceFromUdi(values[0]).Type, toEditor.First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[0]).Unique, toEditor.First().Unique);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[1]).Type, toEditor.Skip(1).First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[1]).Unique, toEditor.Skip(1).First().Unique);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[2]).Type, toEditor.Last().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(values[2]).Unique, toEditor.Last().Unique);
});
}
[Test]
public void Can_Skip_Invalid_Values_To_Editor()
{
var firstGuid = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid());
var secondGuid = new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid());
var values = new[]
{
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString(),
firstGuid.ToString(),
"Invalid Value",
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()).ToString()
secondGuid.ToString(),
};
var toEditor = ToEditor(string.Join(",", values)) as IEnumerable<string>;
Assert.IsNotNull(toEditor);
Assert.AreEqual(2, toEditor.Count());
Assert.AreEqual(values[0], toEditor.First());
Assert.AreEqual(values[2], toEditor.Last());
var toEditor = ToEditor(string.Join(",", values)) as IEnumerable<MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.EditorEntityReference>;
Assert.Multiple(() =>
{
Assert.IsNotNull(toEditor);
Assert.AreEqual(2, toEditor.Count());
Assert.AreEqual(EditorEntityReferenceFromUdi(firstGuid).Type, toEditor.First().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(firstGuid).Unique, toEditor.First().Unique);
Assert.AreEqual(EditorEntityReferenceFromUdi(secondGuid).Type, toEditor.Last().Type);
Assert.AreEqual(EditorEntityReferenceFromUdi(secondGuid).Unique, toEditor.Last().Unique);
});
}
[Test]
@@ -188,26 +227,36 @@ public class MultiNodeTreePickerTests
Assert.IsNull(result);
}
private static object? FromEditor(object? value, int max = 0)
=> CreateValueEditor().FromEditor(new ContentPropertyData(value, new MultipleTextStringConfiguration { Max = max }), null);
private static object? FromEditor(object? value, int max = 0, IJsonSerializer? jsonSerializer = null)
=> CreateValueEditor(jsonSerializer)
.FromEditor(new ContentPropertyData(value, new MultipleTextStringConfiguration { Max = max }), null);
private static object? ToEditor(object? value)
private static object? ToEditor(object? value, IJsonSerializer? jsonSerializer = null)
{
var property = new Mock<IProperty>();
property
.Setup(p => p.GetValue(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool>()))
.Returns(value);
return CreateValueEditor().ToEditor(property.Object);
return CreateValueEditor(jsonSerializer).ToEditor(property.Object);
}
private static MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor CreateValueEditor()
private static MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor CreateValueEditor(
IJsonSerializer? jsonSerializer = null)
{
var valueEditor = new MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor(
Mock.Of<IShortStringHelper>(),
Mock.Of<IJsonSerializer>(),
jsonSerializer ?? Mock.Of<IJsonSerializer>(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("alias"));
return valueEditor;
}
private static MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.EditorEntityReference
EditorEntityReferenceFromUdi(GuidUdi udi) =>
new()
{
Type = udi.EntityType,
Unique = udi.Guid,
};
}