Merge remote-tracking branch 'origin/v9/9.1' into v9/dev

# Conflicts:
#	build/templates/UmbracoPackage/.template.config/template.json
#	build/templates/UmbracoProject/.template.config/template.json
#	src/Directory.Build.props
This commit is contained in:
Bjarke Berg
2021-12-03 07:20:19 +01:00
45 changed files with 1574 additions and 188 deletions

5
tests/.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
root = false
[*.cs]
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_elsewhere = true:none

View File

@@ -766,6 +766,77 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging
Assert.That(testContentType.ContentTypeCompositionExists("Seo"), Is.True);
}
[Test]
public void ImportDocumentType_NewTypeWithOmittedHistoryCleanupPolicy_InsertsDefaultPolicy()
{
// Arrange
var withoutCleanupPolicy = XElement.Parse(ImportResources.SingleDocType);
// Act
var contentTypes = PackageDataInstallation
.ImportDocumentType(withoutCleanupPolicy, 0)
.OfType<IContentTypeWithHistoryCleanup>();
// Assert
Assert.Multiple(() =>
{
Assert.NotNull(contentTypes.Single().HistoryCleanup);
Assert.IsFalse(contentTypes.Single().HistoryCleanup.PreventCleanup);
});
}
[Test]
public void ImportDocumentType_WithHistoryCleanupPolicyElement_ImportsWithCorrectValues()
{
// Arrange
var docTypeElement = XElement.Parse(ImportResources.SingleDocType_WithCleanupPolicy);
// Act
var contentTypes = PackageDataInstallation
.ImportDocumentType(docTypeElement, 0)
.OfType<IContentTypeWithHistoryCleanup>();
// Assert
Assert.Multiple(() =>
{
Assert.NotNull(contentTypes.Single().HistoryCleanup);
Assert.IsTrue(contentTypes.Single().HistoryCleanup.PreventCleanup);
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
});
}
[Test]
public void ImportDocumentType_ExistingTypeWithOmittedHistoryCleanupPolicy_DoesNotOverwriteDatabaseContent()
{
// Arrange
var withoutCleanupPolicy = XElement.Parse(ImportResources.SingleDocType);
var withCleanupPolicy = XElement.Parse(ImportResources.SingleDocType_WithCleanupPolicy);
// Act
var contentTypes = PackageDataInstallation
.ImportDocumentType(withCleanupPolicy, 0)
.OfType<IContentTypeWithHistoryCleanup>();
var contentTypesUpdated = PackageDataInstallation
.ImportDocumentType(withoutCleanupPolicy, 0)
.OfType<IContentTypeWithHistoryCleanup>();
// Assert
Assert.Multiple(() =>
{
Assert.NotNull(contentTypes.Single().HistoryCleanup);
Assert.IsTrue(contentTypes.Single().HistoryCleanup.PreventCleanup);
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
Assert.NotNull(contentTypesUpdated.Single().HistoryCleanup);
Assert.IsTrue(contentTypesUpdated.Single().HistoryCleanup.PreventCleanup);
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
});
}
private void AddLanguages()
{
var globalSettings = new GlobalSettings();

View File

@@ -25,6 +25,7 @@ using Umbraco.Cms.Core.PropertyEditors;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
@@ -225,6 +226,58 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Bytes].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Bytes).Single().Value);
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Extension].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Extension).Single().Value);
}
[Test]
public void Serialize_ForContentTypeWithHistoryCleanupPolicy_OutputsSerializedHistoryCleanupPolicy()
{
// Arrange
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template); // else, FK violation on contentType!
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
contentType.HistoryCleanup = new HistoryCleanup
{
PreventCleanup = true,
KeepAllVersionsNewerThanDays = 1,
KeepLatestVersionPerDayForDays = 2
};
ContentTypeService.Save(contentType);
// Act
var element = Serializer.Serialize(contentType);
// Assert
Assert.Multiple(() =>
{
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("preventCleanup")!.Value, Is.EqualTo("true"));
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("keepAllVersionsNewerThanDays")!.Value, Is.EqualTo("1"));
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("keepLatestVersionPerDayForDays")!.Value, Is.EqualTo("2"));
});
}
[Test]
public void Serialize_ForContentTypeWithNullHistoryCleanupPolicy_DoesNotOutputSerializedDefaultPolicy()
{
// Arrange
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template); // else, FK violation on contentType!
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
contentType.HistoryCleanup = null;
ContentTypeService.Save(contentType);
var element = Serializer.Serialize(contentType);
// Assert
Assert.Multiple(() =>
{
Assert.That(element.Element("HistoryCleanupPolicy"), Is.Null);
});
}
private void CreateDictionaryData()
{

View File

@@ -72,7 +72,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
/// &lt;/info&gt;
/// &lt;Documents&gt;
/// &lt;DocumentSet importMode=&quot;root&quot;&gt;
/// &lt;NewType id=&quot;1148&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;9&quot; createDate=&quot;2013-07-23T12:06:07&quot; updateDate=&quot;2013-07-23T15:56:37&quot; nodeName=&quot;DoIt&quot; urlName=&quot;doit&quot; path=&quot;-1,1148&quot; isDoc=&quot;&quot; nodeType=&quot;1134&quot; creatorName=&quot;admin&quot; writerName=&quot;admin&quot; writerID=&quot;0&quot; template=&quot;1133&quot; nodeTy [rest of string was truncated]&quot;;.
/// &lt;NewType key=&quot;9c9b55d0-2fbf-4f12-afea-023bd7b2519d&quot; id=&quot;1148&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;9&quot; createDate=&quot;2013-07-23T12:06:07&quot; updateDate=&quot;2013-07-23T15:56:37&quot; nodeName=&quot;DoIt&quot; urlName=&quot;doit&quot; path=&quot;-1,1148&quot; isDoc=&quot;&quot; nodeType=&quot;1134&quot; creatorName=&quot;admin&quot; writerName= [rest of string was truncated]&quot;;.
/// </summary>
internal static string CheckboxList_Content_Package {
get {
@@ -91,7 +91,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
/// &lt;/info&gt;
/// &lt;Documents&gt;
/// &lt;DocumentSet importMode=&quot;root&quot;&gt;
/// &lt;umbHomePage id=&quot;1068&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;0&quot; createDate=&quot;2014-11-26T12:52:35&quot; updateDate=&quot;2014-11-26T12:52:36&quot; nodeName=&quot;Home&quot; urlName=&quot;home&quot; path=&quot;-1,1068&quot; isDoc=&quot;&quot; nodeType=&quot;1056&quot; creatorName=&quot;Morten Christensen&quot; writerName=&quot;Morten Christensen&quot; [rest of string was truncated]&quot;;.
/// &lt;umbHomePage key=&quot;9c9b55d0-2fbf-4f12-afea-023bd7b2519d&quot; id=&quot;1068&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;0&quot; createDate=&quot;2014-11-26T12:52:35&quot; updateDate=&quot;2014-11-26T12:52:36&quot; nodeName=&quot;Home&quot; urlName=&quot;home&quot; path=&quot;-1,1068&quot; isDoc=&quot;&quot; nodeType=&quot;1056&quot; creatorName=&quot;Morten Ch [rest of string was truncated]&quot;;.
/// </summary>
internal static string CompositionsTestPackage {
get {
@@ -124,15 +124,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
/// &lt;files /&gt;
/// &lt;info&gt;
/// &lt;package&gt;
/// &lt;name&gt;Dictionary-Package&lt;/name&gt;
/// &lt;name&gt;Dictionary-Package&lt;/name&gt;
/// &lt;/package&gt;
/// &lt;/info&gt;
/// &lt;DictionaryItems&gt;
/// &lt;DictionaryItem Key=&quot;Parent&quot;&gt;
/// &lt;DictionaryItem Key=&quot;28f2e02a-8c66-4fcd-85e3-8524d551c0d3&quot; Name=&quot;Parent&quot;&gt;
/// &lt;Value LanguageId=&quot;2&quot; LanguageCultureAlias=&quot;nb-NO&quot;&gt;&lt;![CDATA[ForelderVerdi]]&gt;&lt;/Value&gt;
/// &lt;Value LanguageId=&quot;3&quot; LanguageCultureAlias=&quot;en-GB&quot;&gt;&lt;![CDATA[ParentValue]]&gt;&lt;/Value&gt;
/// &lt;DictionaryItem Key=&quot;Child&quot;&gt;
/// &lt;Value LanguageId=&quot;2&quot; LanguageCultureAlias=&quot;nb-NO&quot;&gt;&lt;![CDATA[BarnV [rest of string was truncated]&quot;;.
/// &lt;DictionaryItem Key=&quot;e7dba0a9-d517-4ba4-8e18-2764d392c611&quot; Name=&quot; [rest of string was truncated]&quot;;.
/// </summary>
internal static string Dictionary_Package {
get {
@@ -239,6 +238,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
///&lt;DocumentType&gt;
/// &lt;Info&gt;
/// &lt;Name&gt;test&lt;/Name&gt;
/// &lt;Key&gt;150ead17-d359-42a2-ac33-6504cc52ced1&lt;/Key&gt;
/// &lt;Alias&gt;test&lt;/Alias&gt;
/// &lt;Icon&gt;folder.gif&lt;/Icon&gt;
/// &lt;Thumbnail&gt;folder.png&lt;/Thumbnail&gt;
/// &lt;Description&gt;
/// &lt;/Description&gt;
/// &lt;AllowAtRoot&gt;False&lt;/AllowAtRoot&gt;
/// &lt;AllowedTemplates&gt;
/// &lt;Template&gt;test&lt;/Template&gt;
/// &lt;/AllowedTemplates&gt;
/// &lt;DefaultTemplate&gt;test&lt;/DefaultTemplate&gt;
/// &lt;/Info&gt;
/// &lt;Structure&gt;
/// &lt;DocumentType&gt;test&lt;/DocumentType&gt;
/// &lt;/Str [rest of string was truncated]&quot;;.
/// </summary>
internal static string SingleDocType_WithCleanupPolicy {
get {
return ResourceManager.GetString("SingleDocType_WithCleanupPolicy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;
///&lt;umbPackage&gt;
@@ -249,8 +275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
/// &lt;/info&gt;
/// &lt;Documents&gt;
/// &lt;DocumentSet importMode=&quot;root&quot;&gt;
/// &lt;Homepage id=&quot;1072&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;0&quot; createDate=&quot;2013-02-17T09:04:39&quot; updateDate=&quot;2013-02-17T09:10:47&quot; nodeName=&quot;Home&quot; urlName=&quot;home&quot; path=&quot;-1,1072&quot; isDoc=&quot;&quot; nodeType=&quot;1062&quot; creatorName=&quot;admin&quot; writerName=&quot;admin&quot; writerID=&quot;0&quot; template=&quot;1049&quot;&gt;
/// &lt;slide [rest of string was truncated]&quot;;.
/// &lt;Homepage key=&quot;9c9b55d0-2fbf-4f12-afea-023bd7b2519d&quot; id=&quot;1072&quot; parentID=&quot;-1&quot; level=&quot;1&quot; creatorID=&quot;0&quot; sortOrder=&quot;0&quot; createDate=&quot;2013-02-17T09:04:39&quot; updateDate=&quot;2013-02-17T09:10:47&quot; nodeName=&quot;Home&quot; urlName=&quot;home&quot; path=&quot;-1,1072&quot; isDoc=&quot;&quot; nodeType=&quot;1062&quot; creatorName=&quot;admin&quot; writerName=&quot;admin&quot; wr [rest of string was truncated]&quot;;.
/// </summary>
internal static string StandardMvc_Package {
get {

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -157,4 +157,7 @@
<data name="MediaTypesAndMedia_Package.xml" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>MediaTypesAndMedia-Package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="SingleDocType_WithCleanupPolicy" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>SingleDocType-WithCleanupPolicy.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252</value>
</data>
</root>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<DocumentType>
<Info>
<Name>test</Name>
<Key>150ead17-d359-42a2-ac33-6504cc52ced1</Key>
<Alias>test</Alias>
<Icon>folder.gif</Icon>
<Thumbnail>folder.png</Thumbnail>
<Description>
</Description>
<AllowAtRoot>False</AllowAtRoot>
<AllowedTemplates>
<Template>test</Template>
</AllowedTemplates>
<DefaultTemplate>test</DefaultTemplate>
</Info>
<Structure>
<DocumentType>test</DocumentType>
</Structure>
<GenericProperties>
<GenericProperty>
<Name>test</Name>
<Alias>test</Alias>
<Type>b4471851-82b6-4c75-afa4-39fa9c6a75e9</Type>
<Definition>fbaf13a8-4036-41f2-93a3-974f678c312a</Definition>
<Tab>
</Tab>
<Mandatory>False</Mandatory>
<Validation>
</Validation>
<Description><![CDATA[]]></Description>
</GenericProperty>
</GenericProperties>
<Tabs />
<HistoryCleanupPolicy preventCleanup="true" keepAllVersionsNewerThanDays="1" keepLatestVersionPerDayForDays="2" />
</DocumentType>

View File

@@ -0,0 +1,483 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Integration.TestServerTest;
using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Cms.Web.Common.Formatters;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers
{
[TestFixture]
public class EntityControllerTests : UmbracoTestServerTestBase
{
[Test]
public async Task GetUrlsByIds_MediaWithIntegerIds_ReturnsValidMap()
{
IMediaTypeService mediaTypeService = Services.GetRequiredService<IMediaTypeService>();
IMediaService mediaService = Services.GetRequiredService<IMediaService>();
var mediaItems = new List<Media>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IMediaType mediaType = mediaTypeService.Get("image");
mediaTypeService.Save(mediaType);
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
foreach (Media media in mediaItems)
{
mediaService.Save(media);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Media
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
mediaItems[0].Id,
mediaItems[1].Id,
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<int, string> results = await response.Content.ReadFromJsonAsync<IDictionary<int, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
});
}
[Test]
public async Task GetUrlsByIds_Media_ReturnsEmptyStringsInMapForUnknownItems()
{
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Media
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[] { 1, 2 }
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<int, string> results = await response.Content.ReadFromJsonAsync<IDictionary<int, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.That(results!.Keys.Count, Is.EqualTo(2));
Assert.AreEqual(results![payload.ids[0]], string.Empty);
});
}
[Test]
public async Task GetUrlsByIds_MediaWithGuidIds_ReturnsValidMap()
{
IMediaTypeService mediaTypeService = Services.GetRequiredService<IMediaTypeService>();
IMediaService mediaService = Services.GetRequiredService<IMediaService>();
var mediaItems = new List<Media>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IMediaType mediaType = mediaTypeService.Get("image");
mediaTypeService.Save(mediaType);
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
foreach (Media media in mediaItems)
{
mediaService.Save(media);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Media
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
mediaItems[0].Key.ToString(),
mediaItems[1].Key.ToString(),
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<string, string> results = await response.Content.ReadFromJsonAsync<IDictionary<string, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
});
}
[Test]
public async Task GetUrlsByIds_MediaWithUdiIds_ReturnsValidMap()
{
IMediaTypeService mediaTypeService = Services.GetRequiredService<IMediaTypeService>();
IMediaService mediaService = Services.GetRequiredService<IMediaService>();
var mediaItems = new List<Media>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IMediaType mediaType = mediaTypeService.Get("image");
mediaTypeService.Save(mediaType);
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
foreach (Media media in mediaItems)
{
mediaService.Save(media);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Media
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
mediaItems[0].GetUdi().ToString(),
mediaItems[1].GetUdi().ToString(),
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<string, string> results = await response.Content.ReadFromJsonAsync<IDictionary<string, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
});
}
[Test]
public async Task GetUrlsByIds_Documents_ReturnsHashesInMapForUnknownItems()
{
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Document
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[] { 1, 2 }
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<int, string> results = await response.Content.ReadFromJsonAsync<IDictionary<int, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.That(results!.Keys.Count, Is.EqualTo(2));
Assert.AreEqual(results![payload.ids[0]], "#");
});
}
[Test]
public async Task GetUrlsByIds_DocumentWithIntIds_ReturnsValidMap()
{
IContentTypeService contentTypeService = Services.GetRequiredService<IContentTypeService>();
IContentService contentService = Services.GetRequiredService<IContentService>();
var contentItems = new List<IContent>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
contentTypeService.Save(contentType);
ContentBuilder builder = new ContentBuilder()
.WithContentType(contentType);
Content root = builder.WithName("foo").Build();
contentService.SaveAndPublish(root);
contentItems.Add(builder.WithParent(root).WithName("bar").Build());
contentItems.Add(builder.WithParent(root).WithName("baz").Build());
foreach (IContent content in contentItems)
{
contentService.SaveAndPublish(content);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Document
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
contentItems[0].Id,
contentItems[1].Id,
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<int, string> results = await response.Content.ReadFromJsonAsync<IDictionary<int, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
});
}
[Test]
public async Task GetUrlsByIds_DocumentWithGuidIds_ReturnsValidMap()
{
IContentTypeService contentTypeService = Services.GetRequiredService<IContentTypeService>();
IContentService contentService = Services.GetRequiredService<IContentService>();
var contentItems = new List<IContent>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
contentTypeService.Save(contentType);
ContentBuilder builder = new ContentBuilder()
.WithContentType(contentType);
Content root = builder.WithName("foo").Build();
contentService.SaveAndPublish(root);
contentItems.Add(builder.WithParent(root).WithName("bar").Build());
contentItems.Add(builder.WithParent(root).WithName("baz").Build());
foreach (IContent content in contentItems)
{
contentService.SaveAndPublish(content);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Document
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
contentItems[0].Key.ToString(),
contentItems[1].Key.ToString(),
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<string, string> results = await response.Content.ReadFromJsonAsync<IDictionary<string, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
});
}
[Test]
public async Task GetUrlsByIds_DocumentWithUdiIds_ReturnsValidMap()
{
IContentTypeService contentTypeService = Services.GetRequiredService<IContentTypeService>();
IContentService contentService = Services.GetRequiredService<IContentService>();
var contentItems = new List<IContent>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
contentTypeService.Save(contentType);
ContentBuilder builder = new ContentBuilder()
.WithContentType(contentType);
Content root = builder.WithName("foo").Build();
contentService.SaveAndPublish(root);
contentItems.Add(builder.WithParent(root).WithName("bar").Build());
contentItems.Add(builder.WithParent(root).WithName("baz").Build());
foreach (IContent content in contentItems)
{
contentService.SaveAndPublish(content);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Document
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
var payload = new
{
ids = new[]
{
contentItems[0].GetUdi().ToString(),
contentItems[1].GetUdi().ToString(),
}
};
HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
// skip pointless un-parseable cruft.
(await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
IDictionary<string, string> results = await response.Content.ReadFromJsonAsync<IDictionary<string, string>>();
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
});
}
[Test]
public async Task GetByIds_MultipleCalls_WorksAsExpected()
{
IContentTypeService contentTypeService = Services.GetRequiredService<IContentTypeService>();
IContentService contentService = Services.GetRequiredService<IContentService>();
var contentItems = new List<IContent>();
using (ScopeProvider.CreateScope(autoComplete: true))
{
IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
contentTypeService.Save(contentType);
ContentBuilder builder = new ContentBuilder()
.WithContentType(contentType);
Content root = builder.WithName("foo").Build();
contentService.SaveAndPublish(root);
contentItems.Add(builder.WithParent(root).WithName("bar").Build());
contentItems.Add(builder.WithParent(root).WithName("baz").Build());
foreach (IContent content in contentItems)
{
contentService.SaveAndPublish(content);
}
}
var queryParameters = new Dictionary<string, object>
{
["type"] = Constants.UdiEntityType.Document
};
var url = LinkGenerator.GetUmbracoControllerUrl("GetByIds", typeof(EntityController), queryParameters);
var udiPayload = new
{
ids = new[]
{
contentItems[0].GetUdi().ToString(),
contentItems[1].GetUdi().ToString(),
}
};
var intPayload = new
{
ids = new[]
{
contentItems[0].Id,
contentItems[1].Id,
}
};
HttpResponseMessage udiResponse = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, udiPayload);
HttpResponseMessage intResponse = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, intPayload);
Assert.Multiple(() =>
{
Assert.AreEqual(HttpStatusCode.OK, udiResponse.StatusCode, "First request error");
Assert.AreEqual(HttpStatusCode.OK, intResponse.StatusCode, "Second request error");
});
}
}
}

View File

@@ -1,6 +1,10 @@
using System;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Infrastructure.Serialization;
@@ -10,65 +14,35 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
[TestFixture]
public class MemberPasswordHasherTests
{
private MemberPasswordHasher CreateSut() => new MemberPasswordHasher(new LegacyPasswordSecurity(), new JsonNetSerializer());
[Test]
public void VerifyHashedPassword_GivenAnAspNetIdentity2PasswordHash_ThenExpectSuccessRehashNeeded()
[TestCase("Password123!", "AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==", null, ExpectedResult = PasswordVerificationResult.Success, Description = "AspNetCoreIdentityPasswordHash: Correct password")]
[TestCase("wrongPassword", "AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "AspNetCoreIdentityPasswordHash: Wrong password")]
[TestCase("Password123!", "yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=", null, ExpectedResult = PasswordVerificationResult.SuccessRehashNeeded, Description = "GivenALegacyPasswordHash: Correct password")]
[TestCase("wrongPassword", "yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "GivenALegacyPasswordHash: Wrong password")]
[TestCase("Password123!", "AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==", null, ExpectedResult = PasswordVerificationResult.SuccessRehashNeeded, Description = "GivenALegacyPasswordHash: Correct password")]
[TestCase("wrongPassword", "AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "GivenALegacyPasswordHash: Wrong password")]
[TestCase("1234567890", "1234567890", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "ClearText: Correct password, but not supported")]
[TestCase("wrongPassword", "1234567890", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "ClearText: Wrong password")]
[TestCase("1234567890", "XyFRG4/xJ5JGQJYqqIFK70BjHdM=", null, ExpectedResult = PasswordVerificationResult.SuccessRehashNeeded, Description = "Hashed: Correct password")]
[TestCase("wrongPassword", "XyFRG4/xJ5JGQJYqqIFK70BjHdM=", null, ExpectedResult = PasswordVerificationResult.Failed, Description = "Hashed: Wrong password")]
[TestCase("1234567890", "K2JPOhoqNoysfnnD67QsWDSliHrjoSTRTvv9yiaKf30=", "1D43BFA074DF6DCEF6E44A7F5B5F56CDDD60BE198FBBB0222C96A5BD696F3CAA", ExpectedResult = PasswordVerificationResult.SuccessRehashNeeded, Description = "Encrypted: Correct password and correct decryptionKey")]
[TestCase("wrongPassword", "K2JPOhoqNoysfnnD67QsWDSliHrjoSTRTvv9yiaKf30=", "1D43BFA074DF6DCEF6E44A7F5B5F56CDDD60BE198FBBB0222C96A5BD696F3CAA", ExpectedResult = PasswordVerificationResult.Failed, Description = "Encrypted: Wrong password but correct decryptionKey")]
[TestCase("1234567890", "qiuwRr4K7brpTcIzLFfR3iGG9zj4/z4ewHCVZmYUDKM=", "B491B602E0CE1D52450A8089FD2013B340743A7EFCC12B039BD11977A083ACA1", ExpectedResult = PasswordVerificationResult.Failed, Description = "Encrypted: Correct password but wrong decryptionKey")]
[TestCase("1234567890", "qiuwRr4K7brpTcIzLFfR3iGG9zj4/z4ewHCVZmYUDKM=", "InvalidDecryptionKey", ExpectedResult = PasswordVerificationResult.Failed, Description = "Encrypted: Invalid decryptionKey")]
public PasswordVerificationResult VerifyHashedPassword(string password, string encryptedPassword, string decryptionKey)
{
const string password = "Password123!";
const string hash = "AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==";
var member = new MemberIdentityUser() { PasswordConfig = null };
var sut = CreateSut();
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
var sut = new MemberPasswordHasher(
new LegacyPasswordSecurity(),
new JsonNetSerializer(),
Options.Create<LegacyPasswordMigrationSettings>(new LegacyPasswordMigrationSettings()
{
MachineKeyDecryptionKey = decryptionKey
}),
NullLoggerFactory.Instance.CreateLogger<MemberPasswordHasher>());
Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}
[Test]
public void VerifyHashedPassword_GivenAnAspNetCoreIdentityPasswordHash_ThenExpectSuccess()
{
const string password = "Password123!";
const string hash = "AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
Assert.AreEqual(result, PasswordVerificationResult.Success);
}
[Test]
public void VerifyHashedPassword_GivenALegacyPasswordHash_ThenExpectSuccessRehashNeeded()
{
const string password = "Password123!";
const string hash = "yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}
[Test]
public void VerifyHashedPassword_GivenAnUnknownBase64Hash_ThenExpectInvalidOperationException()
{
var hashBytes = new byte[] {3, 2, 1};
var hash = Convert.ToBase64String(hashBytes);
var sut = CreateSut();
Assert.Throws<InvalidOperationException>(() => sut.VerifyHashedPassword(new MemberIdentityUser(), hash, "password"));
}
[TestCase("AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==")]
[TestCase("AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==")]
[TestCase("yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=")]
public void VerifyHashedPassword_GivenAnInvalidPassword_ThenExpectFailure(string hash)
{
const string invalidPassword = "nope";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, invalidPassword);
Assert.AreEqual(result, PasswordVerificationResult.Failed);
return sut.VerifyHashedPassword(member, encryptedPassword, password);
}
}
}

View File

@@ -0,0 +1,113 @@
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoFixture.Kernel;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Smidge;
using Smidge.Cache;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Web.Common.RuntimeMinification;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.RuntimeMinification
{
/// <remarks>
/// UmbracoCustomizations kindly configures an IUmbracoVersion so we need to go verbose without AutoMoqData
/// </remarks>
[TestFixture]
public class UmbracoSmidgeConfigCacheBusterTests
{
[Test]
public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_HasSensibleDefaults()
{
var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
var umbracoVersion = fixture.Freeze<Mock<IUmbracoVersion>>();
var entryAssemblyMetadata = fixture.Freeze<Mock<IEntryAssemblyMetadata>>();
var sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99"));
entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery");
entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99");
var result = sut.GetValue();
var expected = $"Bills.Brilliant.Bakery_9.4.5-beta+41658f99_42.1.2-alpha+41658f99".GenerateHash();
Assert.AreEqual(expected, result);
}
[Test]
public void GetValue_DefaultReleaseSetupWithConfiguredVersion_HasSensibleDefaults()
{
var config = Options.Create(new RuntimeMinificationSettings { Version = "1" });
var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
fixture.Inject(config);
var umbracoVersion = fixture.Freeze<Mock<IUmbracoVersion>>();
var entryAssemblyMetadata = fixture.Freeze<Mock<IEntryAssemblyMetadata>>();
var sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99"));
entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery");
entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99");
var result = sut.GetValue();
var expected = $"1_9.4.5-beta+41658f99_42.1.2-alpha+41658f99".GenerateHash();
Assert.AreEqual(expected, result);
}
[Test]
public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_ChangesWhenUmbracoVersionChanges()
{
var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
var umbracoVersion = fixture.Freeze<Mock<IUmbracoVersion>>();
var entryAssemblyMetadata = fixture.Freeze<Mock<IEntryAssemblyMetadata>>();
var sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99"));
entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery");
entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99");
var before = sut.GetValue();
umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 5, 0, "beta", "41658f99"));
sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
var after = sut.GetValue();
Assert.AreNotEqual(before, after);
}
[Test]
public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_ChangesWhenDownstreamVersionChanges()
{
var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
var umbracoVersion = fixture.Freeze<Mock<IUmbracoVersion>>();
var entryAssemblyMetadata = fixture.Freeze<Mock<IEntryAssemblyMetadata>>();
var sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99"));
entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery");
entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99");
var before = sut.GetValue();
entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.2.0-rc");
sut = fixture.Create<UmbracoSmidgeConfigCacheBuster>();
var after = sut.GetValue();
Assert.AreNotEqual(before, after);
}
}
}