Merge branch 'netcore/dev' of https://github.com/electricsheep/Umbraco-CMS into feature/netcore-unit-tests
This commit is contained in:
@@ -163,6 +163,13 @@ build/hooks/
|
||||
build/temp/
|
||||
|
||||
|
||||
# Acceptance tests
|
||||
cypress.env.json
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/support/chainable.ts
|
||||
/src/Umbraco.Tests.AcceptanceTest/package-lock.json
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/videos/
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/
|
||||
|
||||
|
||||
# eof
|
||||
/src/Umbraco.Web.UI.Client/TESTS-*.xml
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<dependency id="LightInject.Mvc" version="[2.0.0,2.999999)" />
|
||||
<dependency id="LightInject.WebApi" version="[2.0.0,2.999999)" />
|
||||
<dependency id="Markdown" version="[2.2.1,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Identity.Owin" version="[2.2.2,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Identity.Core" version="[2.2.2,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Mvc" version="[5.2.7,5.999999)" />
|
||||
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.WebApi" version="[5.2.7,5.999999)" />
|
||||
|
||||
+2
-1
@@ -381,7 +381,7 @@
|
||||
{
|
||||
Write-Host "Restore NuGet"
|
||||
Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log"
|
||||
$params = "-Source", $nugetsourceUmbraco
|
||||
$params = "-Source", $nugetsourceUmbraco
|
||||
&$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params
|
||||
if (-not $?) { throw "Failed to restore NuGet packages." }
|
||||
})
|
||||
@@ -535,6 +535,7 @@
|
||||
# run
|
||||
if (-not $get)
|
||||
{
|
||||
cd
|
||||
if ($command.Length -eq 0)
|
||||
{
|
||||
$command = @( "Build" )
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Mail;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Configuration
|
||||
@@ -8,5 +9,10 @@ namespace Umbraco.Configuration
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string PickupDirectoryLocation { get; set; }
|
||||
public SmtpDeliveryMethod DeliveryMethod { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
@@ -72,10 +73,10 @@ namespace Umbraco.Configuration.Models
|
||||
_configuration.GetValue(Prefix + "NoNodesViewPath", "~/config/splashes/NoNodes.cshtml");
|
||||
|
||||
public bool IsSmtpServerConfigured =>
|
||||
_configuration.GetSection(Constants.Configuration.ConfigPrefix + "Smtp")?.GetChildren().Any() ?? false;
|
||||
_configuration.GetSection(Constants.Configuration.ConfigGlobalPrefix + "Smtp")?.GetChildren().Any() ?? false;
|
||||
|
||||
public ISmtpSettings SmtpSettings =>
|
||||
new SmtpSettingsImpl(_configuration.GetSection(Constants.Configuration.ConfigPrefix + "Smtp"));
|
||||
new SmtpSettingsImpl(_configuration.GetSection(Constants.Configuration.ConfigGlobalPrefix + "Smtp"));
|
||||
|
||||
private class SmtpSettingsImpl : ISmtpSettings
|
||||
{
|
||||
@@ -90,6 +91,11 @@ namespace Umbraco.Configuration.Models
|
||||
public string Host => _configurationSection.GetValue<string>("Host");
|
||||
public int Port => _configurationSection.GetValue<int>("Port");
|
||||
public string PickupDirectoryLocation => _configurationSection.GetValue<string>("PickupDirectoryLocation");
|
||||
public SmtpDeliveryMethod DeliveryMethod => _configurationSection.GetValue<SmtpDeliveryMethod>("DeliveryMethod");
|
||||
|
||||
public string Username => _configurationSection.GetValue<string>("Username");
|
||||
|
||||
public string Password => _configurationSection.GetValue<string>("Password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
public static class ClaimsIdentityExtensions
|
||||
{
|
||||
public static string GetUserId(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string userId = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? claimsIdentity.FindFirstValue("sub");
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
public static string GetUserName(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string username = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
username = claimsIdentity.FindFirstValue(ClaimTypes.Name)
|
||||
?? claimsIdentity.FindFirstValue("preferred_username");
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
return identity.FindFirst(claimType)?.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Umbraco.Core.Configuration
|
||||
{
|
||||
public interface ISmtpSettings
|
||||
@@ -6,5 +8,8 @@ namespace Umbraco.Core.Configuration
|
||||
string Host { get; }
|
||||
int Port{ get; }
|
||||
string PickupDirectoryLocation { get; }
|
||||
SmtpDeliveryMethod DeliveryMethod { get; }
|
||||
string Username { get; }
|
||||
string Password { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
/// The route name of the page shown when Umbraco has no published content.
|
||||
/// </summary>
|
||||
public const string NoContentRouteName = "umbraco-no-content";
|
||||
|
||||
/// <summary>
|
||||
/// The claim type for the ASP.NET Identity security stamp
|
||||
/// </summary>
|
||||
public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
|
||||
|
||||
/// <summary>
|
||||
/// The default authentication type used for remembering that 2FA is not needed on next login
|
||||
/// </summary>
|
||||
public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Umbraco.Core.Composing;
|
||||
@@ -77,15 +78,28 @@ namespace Umbraco.Core
|
||||
|
||||
var ctorParameters = ctor.GetParameters();
|
||||
var ctorArgs = new object[ctorParameters.Length];
|
||||
var availableArgs = new List<object>(args);
|
||||
var i = 0;
|
||||
foreach (var parameter in ctorParameters)
|
||||
{
|
||||
// no! IsInstanceOfType is not ok here
|
||||
// ReSharper disable once UseMethodIsInstanceOfType
|
||||
var arg = args?.FirstOrDefault(a => parameter.ParameterType.IsAssignableFrom(a.GetType()));
|
||||
ctorArgs[i++] = arg ?? factory.GetInstance(parameter.ParameterType);
|
||||
var idx = availableArgs.FindIndex(a => parameter.ParameterType.IsAssignableFrom(a.GetType()));
|
||||
if(idx >= 0)
|
||||
{
|
||||
// Found a suitable supplied argument
|
||||
ctorArgs[i++] = availableArgs[idx];
|
||||
|
||||
// A supplied argument can be used at most once
|
||||
availableArgs.RemoveAt(idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// None of the provided arguments is suitable: get an instance from the factory
|
||||
ctorArgs[i++] = factory.GetInstance(parameter.ParameterType);
|
||||
}
|
||||
}
|
||||
return ctor.Invoke(ctorArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,8 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
|
||||
[DataMember(Name = "allowCultureVariant")]
|
||||
public bool AllowCultureVariant { get; set; }
|
||||
|
||||
[DataMember(Name = "allowSegmentVariant")]
|
||||
public bool AllowSegmentVariant { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
/// </summary>
|
||||
public string Culture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When dealing with content variants, this is the segment for the variant
|
||||
/// </summary>
|
||||
public string Segment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An array of metadata that is parsed out from the file info posted to the server which is set on the client.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public static class RelationTypeExtensions
|
||||
{
|
||||
public static bool IsSystemRelationType(this IRelationType relationType) =>
|
||||
relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias
|
||||
|| relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias
|
||||
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias
|
||||
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias
|
||||
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Actions;
|
||||
using System.Threading;
|
||||
|
||||
namespace Umbraco.Web.Models.Trees
|
||||
{
|
||||
@@ -28,12 +29,15 @@ namespace Umbraco.Web.Models.Trees
|
||||
Name = name;
|
||||
}
|
||||
|
||||
|
||||
public MenuItem(string alias, ILocalizedTextService textService)
|
||||
: this()
|
||||
{
|
||||
var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture);
|
||||
values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription);
|
||||
|
||||
Alias = alias;
|
||||
Name = textService.Localize($"actions/{Alias}");
|
||||
TextDescription = textDescription;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,6 +78,9 @@ namespace Umbraco.Web.Models.Trees
|
||||
[Required]
|
||||
public string Alias { get; set; }
|
||||
|
||||
[DataMember(Name = "textDescription")]
|
||||
public string TextDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a menu separator will exist before this menu item
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Actions;
|
||||
|
||||
@@ -50,14 +51,17 @@ namespace Umbraco.Web.Models.Trees
|
||||
var item = _actionCollection.GetAction<T>();
|
||||
if (item == null) return null;
|
||||
|
||||
var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture);
|
||||
values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription);
|
||||
|
||||
var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}"))
|
||||
{
|
||||
SeparatorBefore = hasSeparator,
|
||||
OpensDialog = opensDialog
|
||||
OpensDialog = opensDialog,
|
||||
TextDescription = textDescription,
|
||||
};
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataMember(Name = "allowCultureVariant")]
|
||||
public bool AllowCultureVariant { get; set; }
|
||||
|
||||
[DataMember(Name = "allowSegmentVariant")]
|
||||
public bool AllowSegmentVariant { get; set; }
|
||||
|
||||
//Tabs
|
||||
[DataMember(Name = "groups")]
|
||||
public IEnumerable<PropertyGroupBasic<TPropertyType>> Groups { get; set; }
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataMember(Name = "name", IsRequired = true)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[DataMember(Name = "displayName")]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the tabs containing display properties
|
||||
/// </summary>
|
||||
|
||||
@@ -20,9 +20,12 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
|
||||
[DataMember(Name = "defaultTemplate")]
|
||||
public EntityBasic DefaultTemplate { get; set; }
|
||||
|
||||
|
||||
[DataMember(Name = "allowCultureVariant")]
|
||||
public bool AllowCultureVariant { get; set; }
|
||||
|
||||
[DataMember(Name = "allowSegmentVariant")]
|
||||
public bool AllowSegmentVariant { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
Notifications = new List<BackOfficeNotification>();
|
||||
}
|
||||
|
||||
[DataMember(Name = "isSystemRelationType")]
|
||||
public bool IsSystemRelationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false)
|
||||
/// </summary>
|
||||
|
||||
@@ -132,6 +132,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
MapTypeToDisplayBase<DocumentTypeDisplay, PropertyTypeDisplay>(source, target);
|
||||
|
||||
target.AllowCultureVariant = source.VariesByCulture();
|
||||
target.AllowSegmentVariant = source.VariesBySegment();
|
||||
|
||||
//sync templates
|
||||
target.AllowedTemplates = context.MapEnumerable<ITemplate, EntityBasic>(source.AllowedTemplates);
|
||||
@@ -245,6 +246,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.ValidationRegExp = source.Validation.Pattern;
|
||||
target.ValidationRegExpMessage = source.Validation.PatternMessage;
|
||||
target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant);
|
||||
target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant);
|
||||
|
||||
if (source.Id > 0)
|
||||
target.Id = source.Id;
|
||||
@@ -356,6 +358,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
{
|
||||
target.Alias = source.Alias;
|
||||
target.AllowCultureVariant = source.AllowCultureVariant;
|
||||
target.AllowSegmentVariant = source.AllowSegmentVariant;
|
||||
target.DataTypeId = source.DataTypeId;
|
||||
target.DataTypeKey = source.DataTypeKey;
|
||||
target.Description = source.Description;
|
||||
@@ -372,6 +375,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
{
|
||||
target.Alias = source.Alias;
|
||||
target.AllowCultureVariant = source.AllowCultureVariant;
|
||||
target.AllowSegmentVariant = source.AllowSegmentVariant;
|
||||
target.DataTypeId = source.DataTypeId;
|
||||
target.DataTypeKey = source.DataTypeKey;
|
||||
target.Description = source.Description;
|
||||
@@ -417,6 +421,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
if (!(target is IMemberType))
|
||||
{
|
||||
target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant);
|
||||
target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant);
|
||||
}
|
||||
|
||||
// handle property groups and property types
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Mapping;
|
||||
@@ -13,10 +14,12 @@ namespace Umbraco.Web.Models.Mapping
|
||||
public class ContentVariantMapper
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
|
||||
public ContentVariantMapper(ILocalizationService localizationService)
|
||||
public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService)
|
||||
{
|
||||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||
_localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService));
|
||||
}
|
||||
|
||||
public IEnumerable<ContentVariantDisplay> Map(IContent source, MapperContext context)
|
||||
@@ -113,11 +116,19 @@ namespace Umbraco.Web.Models.Mapping
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <returns>
|
||||
/// Returns all segments assigned to the content including 'null' values
|
||||
/// Returns all segments assigned to the content including the default `null` segment.
|
||||
/// </returns>
|
||||
private IEnumerable<string> GetSegments(IContent content)
|
||||
{
|
||||
return content.Properties.SelectMany(p => p.Values.Select(v => v.Segment)).Distinct();
|
||||
// The default segment (null) is always there,
|
||||
// even when there is no property data at all yet
|
||||
var segments = new List<string> { null };
|
||||
|
||||
// Add actual segments based on the property values
|
||||
segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment)));
|
||||
|
||||
// Do not return a segment more than once
|
||||
return segments.Distinct();
|
||||
}
|
||||
|
||||
private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, Language language, string segment)
|
||||
@@ -130,8 +141,31 @@ namespace Umbraco.Web.Models.Mapping
|
||||
variantDisplay.Segment = segment;
|
||||
variantDisplay.Language = language;
|
||||
variantDisplay.Name = content.GetCultureName(language?.IsoCode);
|
||||
variantDisplay.DisplayName = GetDisplayName(language, segment);
|
||||
|
||||
return variantDisplay;
|
||||
}
|
||||
|
||||
private string GetDisplayName(Language language, string segment)
|
||||
{
|
||||
var isCultureVariant = language != null;
|
||||
var isSegmentVariant = !segment.IsNullOrWhiteSpace();
|
||||
|
||||
if(!isCultureVariant && !isSegmentVariant)
|
||||
{
|
||||
return _localizedTextService.Localize("general/default");
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
|
||||
if (isSegmentVariant)
|
||||
parts.Add(segment);
|
||||
|
||||
if (isCultureVariant)
|
||||
parts.Add(language.Name);
|
||||
|
||||
return string.Join(" — ", parts);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,8 @@ namespace Umbraco.Web.Models.Mapping
|
||||
SortOrder = p.SortOrder,
|
||||
ContentTypeId = contentType.Id,
|
||||
ContentTypeName = contentType.Name,
|
||||
AllowCultureVariant = p.VariesByCulture()
|
||||
AllowCultureVariant = p.VariesByCulture(),
|
||||
AllowSegmentVariant = p.VariesBySegment()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key);
|
||||
target.Path = "-1," + source.Id;
|
||||
|
||||
target.IsSystemRelationType = source.IsSystemRelationType();
|
||||
|
||||
// Set the "friendly" and entity names for the parent and child object types
|
||||
if (source.ParentObjectType.HasValue)
|
||||
{
|
||||
|
||||
@@ -124,15 +124,16 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
// publishing = deal with edit and published values
|
||||
foreach (var propertyValue in property.Values)
|
||||
{
|
||||
var isInvariantValue = propertyValue.Culture == null;
|
||||
var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null;
|
||||
var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null;
|
||||
var isCultureValue = propertyValue.Culture != null;
|
||||
var isSegmentValue = propertyValue.Segment != null;
|
||||
|
||||
// deal with published value
|
||||
if (propertyValue.PublishedValue != null && publishedVersionId > 0)
|
||||
if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0)
|
||||
propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue));
|
||||
|
||||
// deal with edit value
|
||||
if (propertyValue.EditedValue != null)
|
||||
if (propertyValue.EditedValue != null || isSegmentValue)
|
||||
propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue));
|
||||
|
||||
// property.Values will contain ALL of it's values, both variant and invariant which will be populated if the
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
|
||||
public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
|
||||
|
||||
public NestedContentPropertyEditor(
|
||||
ILogger logger,
|
||||
|
||||
@@ -320,6 +320,7 @@ namespace Umbraco.Core.Runtime
|
||||
composition.RegisterUnique<IEventMessagesAccessor, HybridEventMessagesAccessor>();
|
||||
composition.RegisterUnique<ITreeService, TreeService>();
|
||||
composition.RegisterUnique<ISectionService, SectionService>();
|
||||
composition.RegisterUnique<IEmailSender, EmailSender>();
|
||||
|
||||
composition.RegisterUnique<IExamineManager, ExamineManager>();
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Configuration;
|
||||
using System.Web;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.Dtos;
|
||||
@@ -17,9 +20,10 @@ namespace Umbraco.Core.Runtime
|
||||
public class SqlMainDomLock : IMainDomLock
|
||||
{
|
||||
private string _lockId;
|
||||
private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private IUmbracoDatabase _db;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
|
||||
@@ -28,17 +32,20 @@ namespace Umbraco.Core.Runtime
|
||||
private bool _hasError;
|
||||
private object _locker = new object();
|
||||
|
||||
public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator)
|
||||
public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
// unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
|
||||
_lockId = Guid.NewGuid().ToString();
|
||||
_logger = logger;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_dbFactory = new UmbracoDatabaseFactory(_logger,
|
||||
globalSettings,
|
||||
connectionStrings,
|
||||
Constants.System.UmbracoConnectionName,
|
||||
new Lazy<IMapperCollection>(() => new MapperCollection(Enumerable.Empty<BaseMapper>())),
|
||||
dbProviderFactoryCreator);
|
||||
|
||||
MainDomKey = MainDomKeyPrefix + "-" + (NetworkHelper.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash<SHA1>();
|
||||
}
|
||||
|
||||
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
@@ -128,6 +135,16 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the keyvalue table key for the current server/app
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key is the the normal MainDomId which takes into account the AppDomainAppId and the physical file path of the app and this is
|
||||
/// combined with the current machine name. The machine name is required because the default semaphore lock is machine wide so it implicitly
|
||||
/// takes into account machine name whereas this needs to be explicitly per machine.
|
||||
/// </remarks>
|
||||
private string MainDomKey { get; }
|
||||
|
||||
private void ListeningLoop()
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="LightInject.Annotation" Version="1.1.0" />
|
||||
<PackageReference Include="LightInject.Microsoft.DependencyInjection" Version="3.3.0" />
|
||||
<PackageReference Include="LightInject.Microsoft.Hosting" Version="1.2.0" />
|
||||
<PackageReference Include="MailKit" Version="2.6.0" />
|
||||
<PackageReference Include="Markdown" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Composing;
|
||||
using MimeKit;
|
||||
using MimeKit.Text;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Events;
|
||||
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
@@ -21,7 +24,7 @@ namespace Umbraco.Core
|
||||
{
|
||||
}
|
||||
|
||||
internal EmailSender(IGlobalSettings globalSettings, bool enableEvents)
|
||||
public EmailSender(IGlobalSettings globalSettings, bool enableEvents)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_enableEvents = enableEvents;
|
||||
@@ -45,7 +48,17 @@ namespace Umbraco.Core
|
||||
{
|
||||
using (var client = new SmtpClient())
|
||||
{
|
||||
client.Send(message);
|
||||
|
||||
client.Connect(_globalSettings.SmtpSettings.Host, _globalSettings.SmtpSettings.Port);
|
||||
|
||||
if (!(_globalSettings.SmtpSettings.Username is null &&
|
||||
_globalSettings.SmtpSettings.Password is null))
|
||||
{
|
||||
client.Authenticate(_globalSettings.SmtpSettings.Username, _globalSettings.SmtpSettings.Password);
|
||||
}
|
||||
|
||||
client.Send(ConstructEmailMessage(message));
|
||||
client.Disconnect(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,14 +78,25 @@ namespace Umbraco.Core
|
||||
{
|
||||
using (var client = new SmtpClient())
|
||||
{
|
||||
if (client.DeliveryMethod == SmtpDeliveryMethod.Network)
|
||||
await client.ConnectAsync(_globalSettings.SmtpSettings.Host, _globalSettings.SmtpSettings.Port);
|
||||
|
||||
if (!(_globalSettings.SmtpSettings.Username is null &&
|
||||
_globalSettings.SmtpSettings.Password is null))
|
||||
{
|
||||
await client.SendMailAsync(message);
|
||||
await client.AuthenticateAsync(_globalSettings.SmtpSettings.Username, _globalSettings.SmtpSettings.Password);
|
||||
}
|
||||
|
||||
var mailMessage = ConstructEmailMessage(message);
|
||||
if (_globalSettings.SmtpSettings.DeliveryMethod == SmtpDeliveryMethod.Network)
|
||||
{
|
||||
await client.SendAsync(mailMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
client.Send(message);
|
||||
client.Send(mailMessage);
|
||||
}
|
||||
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +107,7 @@ namespace Umbraco.Core
|
||||
/// <remarks>
|
||||
/// We assume this is possible if either an event handler is registered or an smtp server is configured
|
||||
/// </remarks>
|
||||
internal static bool CanSendRequiredEmail(IGlobalSettings globalSettings) => EventHandlerRegistered || globalSettings.IsSmtpServerConfigured;
|
||||
public static bool CanSendRequiredEmail(IGlobalSettings globalSettings) => EventHandlerRegistered || globalSettings.IsSmtpServerConfigured;
|
||||
|
||||
/// <summary>
|
||||
/// returns true if an event handler has been registered
|
||||
@@ -103,5 +127,22 @@ namespace Umbraco.Core
|
||||
var handler = SendEmail;
|
||||
if (handler != null) handler(null, e);
|
||||
}
|
||||
|
||||
private MimeMessage ConstructEmailMessage(MailMessage mailMessage)
|
||||
{
|
||||
var fromEmail = mailMessage.From?.Address;
|
||||
if(string.IsNullOrEmpty(fromEmail))
|
||||
fromEmail = _globalSettings.SmtpSettings.From;
|
||||
|
||||
var messageToSend = new MimeMessage
|
||||
{
|
||||
Subject = mailMessage.Subject,
|
||||
From = { new MailboxAddress(fromEmail)},
|
||||
Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }
|
||||
};
|
||||
messageToSend.To.AddRange(mailMessage.To.Select(x=>new MailboxAddress(x.Address)));
|
||||
|
||||
return messageToSend;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
/// </summary>
|
||||
internal class ContentNestedData
|
||||
{
|
||||
[JsonProperty("properties")]
|
||||
//dont serialize empty properties
|
||||
[JsonProperty("pd")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
|
||||
public Dictionary<string, PropertyData[]> PropertyData { get; set; }
|
||||
|
||||
[JsonProperty("cultureData")]
|
||||
[JsonProperty("cd")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
public Dictionary<string, CultureVariation> CultureData { get; set; }
|
||||
|
||||
[JsonProperty("urlSegment")]
|
||||
[JsonProperty("us")]
|
||||
public string UrlSegment { get; set; }
|
||||
|
||||
//Legacy properties used to deserialize existing nucache db entries
|
||||
[JsonProperty("properties")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
|
||||
private Dictionary<string, PropertyData[]> LegacyPropertyData { set { PropertyData = value; } }
|
||||
|
||||
[JsonProperty("cultureData")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
private Dictionary<string, CultureVariation> LegacyCultureData { set { CultureData = value; } }
|
||||
|
||||
[JsonProperty("urlSegment")]
|
||||
private string LegacyUrlSegment { set { UrlSegment = value; } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,29 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
/// </summary>
|
||||
public class CultureVariation
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
[JsonProperty("nm")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("urlSegment")]
|
||||
[JsonProperty("us")]
|
||||
public string UrlSegment { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
[JsonProperty("dt")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[JsonProperty("isDraft")]
|
||||
[JsonProperty("isd")]
|
||||
public bool IsDraft { get; set; }
|
||||
|
||||
//Legacy properties used to deserialize existing nucache db entries
|
||||
[JsonProperty("name")]
|
||||
private string LegacyName { set { Name = value; } }
|
||||
|
||||
[JsonProperty("urlSegment")]
|
||||
private string LegacyUrlSegment { set { UrlSegment = value; } }
|
||||
|
||||
[JsonProperty("date")]
|
||||
private DateTime LegacyDate { set { Date = value; } }
|
||||
|
||||
[JsonProperty("isDraft")]
|
||||
private bool LegacyIsDraft { set { IsDraft = value; } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
@@ -8,21 +9,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
private string _culture;
|
||||
private string _segment;
|
||||
|
||||
[JsonProperty("culture")]
|
||||
[DefaultValue("")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")]
|
||||
public string Culture
|
||||
{
|
||||
get => _culture;
|
||||
set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
|
||||
}
|
||||
|
||||
[JsonProperty("seg")]
|
||||
[DefaultValue("")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")]
|
||||
public string Segment
|
||||
{
|
||||
get => _segment;
|
||||
set => _segment = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
|
||||
}
|
||||
|
||||
[JsonProperty("val")]
|
||||
[JsonProperty("v")]
|
||||
public object Value { get; set; }
|
||||
|
||||
|
||||
//Legacy properties used to deserialize existing nucache db entries
|
||||
[JsonProperty("culture")]
|
||||
private string LegacyCulture
|
||||
{
|
||||
set => Culture = value;
|
||||
}
|
||||
|
||||
[JsonProperty("seg")]
|
||||
private string LegacySegment
|
||||
{
|
||||
set => Segment = value;
|
||||
}
|
||||
|
||||
[JsonProperty("val")]
|
||||
private object LegacyValue
|
||||
{
|
||||
set => Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,22 +901,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// they require.
|
||||
|
||||
// These can be run side by side in parallel.
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _);
|
||||
}
|
||||
|
||||
Parallel.Invoke(
|
||||
() =>
|
||||
{
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _);
|
||||
}
|
||||
});
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _);
|
||||
}
|
||||
}
|
||||
|
||||
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Umbraco Acceptance Tests
|
||||
|
||||
### Prerequisite
|
||||
- NodeJS 12+
|
||||
- A running installed Umbraco on url: [https://localhost:44331](https://localhost:44331) (Default development port)
|
||||
- Install using a `SqlServer`/`LocalDb` as the tests execute too fast for `SqlCE` to handle.
|
||||
- User information in `cypress.env.json` (See [Getting started](#getting-started))
|
||||
|
||||
### Getting started
|
||||
The tests is located in the project/folder named `Umbraco.Tests.AcceptanceTests`. Ensur to run `npm install` in that folder, or let your IDE do that.
|
||||
|
||||
Next, it is important you create a new file in the root of the project called `cypress.env.json`.
|
||||
This file is already added to `.gitignore` and can contain values that is different for each developer machine.
|
||||
|
||||
The file need the following content:
|
||||
```
|
||||
{
|
||||
"username": "<email for superadmin>",
|
||||
"password": "<password for superadmin>"
|
||||
}
|
||||
```
|
||||
Replace the `<email for superadmin>` and `<password for superadmin>` placeholders with correct info.
|
||||
|
||||
|
||||
|
||||
### Executing tests
|
||||
|
||||
There exists two npm scripts, that can be used to execute the test.
|
||||
|
||||
1. `npm run test`
|
||||
- Executes the tests headless.
|
||||
1. `npm run ui`
|
||||
- Executes the tests in a browser handled by a cypress application.
|
||||
|
||||
In case of errors it is recommended to use the UI to debug.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"baseUrl": "https://localhost:44331",
|
||||
"viewportHeight": 1024,
|
||||
"viewportWidth": 1200,
|
||||
"env": {
|
||||
"username": "<insert username/email in cypress.env.json>",
|
||||
"password": "<insert password in cypress.env.json>"
|
||||
},
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videoUploadOnPasses" : false
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Login', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/umbraco');
|
||||
});
|
||||
|
||||
it('Login with correct username and password', () => {
|
||||
const username = Cypress.env('username');
|
||||
const password = Cypress.env('password');
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.url().should('include', '/umbraco#/content')
|
||||
cy.get('#umb-username').should('not.exist');
|
||||
cy.get('#umb-passwordTwo').should('not.exist');
|
||||
});
|
||||
|
||||
|
||||
it('Login with correct username but wrong password', () => {
|
||||
const username = Cypress.env('username');
|
||||
const password = 'wrong';
|
||||
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.get('.text-error').should('exist');
|
||||
cy.get('#umb-username').should('exist');
|
||||
cy.get('#umb-passwordTwo').should('exist');
|
||||
});
|
||||
|
||||
it('Login with wrong username and wrong password', () => {
|
||||
const username = 'wrong-username';
|
||||
const password = 'wrong';
|
||||
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.get('.text-error').should('exist');
|
||||
cy.get('#umb-username').should('exist');
|
||||
cy.get('#umb-passwordTwo').should('exist');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference types="Cypress" />
|
||||
import {LabelDataTypeBuilder} from 'umbraco-cypress-testhelpers';
|
||||
context('Data Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create data type', () => {
|
||||
const name = "Test data type";
|
||||
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Data Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.umbracoContextMenuAction("action-data-type").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('select[name="selectedEditor"]').select('Label');
|
||||
|
||||
cy.get('.umb-property-editor select').select('Time');
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
it('Delete data type', () => {
|
||||
const name = "Test data type";
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
const dataType = new LabelDataTypeBuilder()
|
||||
.withSaveNewAction()
|
||||
.withName(name)
|
||||
.build();
|
||||
|
||||
cy.saveDataType(dataType);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Data Types", name]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-delete").click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("general_delete").click();
|
||||
|
||||
cy.contains(name).should('not.exist');
|
||||
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/// <reference types="Cypress" />
|
||||
import { DocumentTypeBuilder } from 'umbraco-cypress-testhelpers';
|
||||
context('Document Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create document type', () => {
|
||||
const name = "Test document type";
|
||||
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Document Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.umbracoContextMenuAction("action-documentType").click();
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
it('Delete document type', () => {
|
||||
const name = "Test document type";
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
const dataType = new DocumentTypeBuilder()
|
||||
.withName(name)
|
||||
.build();
|
||||
|
||||
cy.saveDocumentType(dataType);
|
||||
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Document Types", name]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-delete").click();
|
||||
|
||||
cy.get('label.checkbox').click();
|
||||
cy.umbracoButtonByLabelKey("general_ok").click();
|
||||
|
||||
cy.contains(name).should('not.exist');
|
||||
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Languages', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Add language', () => {
|
||||
const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box
|
||||
|
||||
cy.umbracoEnsureLanguageNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Languages"]).click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("languages_addLanguage").click();
|
||||
|
||||
cy.get('select[name="newLang"]').select(name);
|
||||
|
||||
// //Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureLanguageNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Macros', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create macro', () => {
|
||||
const name = "Test macro";
|
||||
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Macros"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
cy.get('form[name="createMacroForm"]').within(($form) => {
|
||||
cy.get('input[name="itemKey"]').type(name);
|
||||
cy.get(".btn-primary").click();
|
||||
});
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.hash).to.include('#/settings/macros/edit/')
|
||||
});
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Media Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create media type', () => {
|
||||
const name = "Test media type";
|
||||
|
||||
cy.umbracoEnsureMediaTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Media Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMediaTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Member Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create member type', () => {
|
||||
const name = "Test member type";
|
||||
|
||||
cy.umbracoEnsureMemberTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Member Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMemberTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Partial View Macro Files', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new partial view macro', () => {
|
||||
const name = "TestPartialViewMacro";
|
||||
const fileName = name + ".cshtml";
|
||||
|
||||
cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName);
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Partial View Macro Files"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-label").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName);
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Partial Views', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new empty partial view', () => {
|
||||
const name = "TestPartialView";
|
||||
const fileName = name + ".cshtml";
|
||||
|
||||
cy.umbracoEnsurePartialViewNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Partial Views"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsurePartialViewNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Relation Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create relation type', () => {
|
||||
const name = "Test relation type";
|
||||
|
||||
cy.umbracoEnsureRelationTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Relation Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
cy.get('form[name="createRelationTypeForm"]').within(($form) => {
|
||||
cy.get('input[name="relationTypeName"]').type(name);
|
||||
|
||||
cy.get('[name="relationType-direction"] input').first().click({force:true});
|
||||
|
||||
cy.get('select[name="relationType-parent"]').select('Document');
|
||||
|
||||
cy.get('select[name="relationType-child"]').select('Media');
|
||||
|
||||
cy.get(".btn-primary").click();
|
||||
});
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.hash).to.include('#/settings/relationTypes/edit/')
|
||||
})
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureRelationTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Scripts', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new JavaScript file', () => {
|
||||
const name = "TestScript";
|
||||
const fileName = name + ".js";
|
||||
|
||||
cy.umbracoEnsureScriptNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureScriptNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Stylesheets', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new style sheet file', () => {
|
||||
const name = "TestStylesheet";
|
||||
const fileName = name + ".css";
|
||||
|
||||
cy.umbracoEnsureStylesheetNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureStylesheetNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/// <reference types="Cypress" />
|
||||
import {DocumentTypeBuilder, TemplateBuilder} from "umbraco-cypress-testhelpers";
|
||||
|
||||
context('Templates', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create template', () => {
|
||||
const name = "Test template";
|
||||
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Templates"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get("form[name='contentForm']").submit();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
});
|
||||
|
||||
it('Delete template', () => {
|
||||
const name = "Test template";
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
|
||||
const template = new TemplateBuilder()
|
||||
.withName(name)
|
||||
.build();
|
||||
|
||||
cy.saveTemplate(template);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Templates", name]).rightclick();
|
||||
cy.umbracoContextMenuAction("action-delete").click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("general_ok").click();
|
||||
|
||||
cy.contains(name).should('not.exist');
|
||||
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
context('User Groups', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create user group', () => {
|
||||
const name = "Test Group";
|
||||
|
||||
cy.umbracoEnsureUserGroupNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('users');
|
||||
cy.get('[data-element="sub-view-userGroups"]').click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("actions_createGroup").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
// Assign sections
|
||||
cy.get('.umb-box:nth-child(1) .umb-property:nth-child(1) localize').click();
|
||||
cy.get('.umb-tree-item span').click({multiple:true});
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
// Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureUserGroupNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Users', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create user', () => {
|
||||
const name = "Alice Bobson";
|
||||
const email = "alice-bobson@acceptancetest.umbraco";
|
||||
|
||||
cy.umbracoEnsureUserEmailNotExists(email);
|
||||
cy.umbracoSection('users');
|
||||
cy.umbracoButtonByLabelKey("user_createUser").click();
|
||||
|
||||
|
||||
cy.get('input[name="name"]').type(name);
|
||||
cy.get('input[name="email"]').type(email);
|
||||
|
||||
cy.get('.umb-node-preview-add').click();
|
||||
cy.get('.umb-user-group-picker-list-item:nth-child(1) > .umb-user-group-picker__action').click();
|
||||
cy.get('.umb-user-group-picker-list-item:nth-child(2) > .umb-user-group-picker__action').click();
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
cy.get('.umb-button > .btn > .umb-button__content').click();
|
||||
|
||||
|
||||
cy.umbracoButtonByLabelKey("user_goToProfile").should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureUserEmailNotExists(email);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import {Command} from 'umbraco-cypress-testhelpers';
|
||||
import {Chainable} from './chainable';
|
||||
new Chainable();
|
||||
new Command().registerCypressCommands();
|
||||
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"../node_modules/cypress",
|
||||
"*/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// type definitions for Cypress object "cy"
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// type definitions for custom commands like "createDefaultTodos"
|
||||
// <reference types="support" />
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"test": "npx cypress run",
|
||||
"ui": "npx cypress open"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.2",
|
||||
"ncp": "^2.0.0",
|
||||
"cypress": "^4.6.0",
|
||||
"umbraco-cypress-testhelpers": "1.0.0-beta-39"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^3.9.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./lib",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"target": "es5",
|
||||
|
||||
"types": [
|
||||
"cypress"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom"
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-tslint-plugin",
|
||||
"alwaysShowRuleFailuresAsWarnings": false,
|
||||
"ignoreDefinitionFiles": true,
|
||||
"configFile": "tslint.json",
|
||||
"suppressWhileTypeErrorsPresent": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["tslint:recommended", "tslint-config-prettier"]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Umbraco.Core.Configuration;
|
||||
using System.Net.Mail;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Tests.Common.Builders
|
||||
{
|
||||
@@ -16,6 +18,9 @@ namespace Umbraco.Tests.Common.Builders
|
||||
private string _host;
|
||||
private int? _port;
|
||||
private string _pickupDirectoryLocation;
|
||||
private SmtpDeliveryMethod? _deliveryMethod;
|
||||
private string _username;
|
||||
private string _password;
|
||||
|
||||
public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder)
|
||||
{
|
||||
@@ -33,24 +38,45 @@ namespace Umbraco.Tests.Common.Builders
|
||||
return this;
|
||||
}
|
||||
|
||||
public SmtpSettingsBuilder<TParent> WithUsername(string username)
|
||||
{
|
||||
_username = username;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SmtpSettingsBuilder<TParent> WithPost(int port)
|
||||
{
|
||||
_port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SmtpSettingsBuilder<TParent> WithPassword(string password)
|
||||
{
|
||||
_password = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SmtpSettingsBuilder<TParent> WithPickupDirectoryLocation(string pickupDirectoryLocation)
|
||||
{
|
||||
_pickupDirectoryLocation = pickupDirectoryLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SmtpSettingsBuilder<TParent> WithDeliveryMethod(SmtpDeliveryMethod deliveryMethod)
|
||||
{
|
||||
_deliveryMethod = deliveryMethod;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override ISmtpSettings Build()
|
||||
{
|
||||
var from = _from ?? null;
|
||||
var host = _host ?? null;
|
||||
var port = _port ?? 25;
|
||||
var pickupDirectoryLocation = _pickupDirectoryLocation ?? null;
|
||||
var deliveryMethod = _deliveryMethod ?? SmtpDeliveryMethod.Network;
|
||||
var username = _username ?? null;
|
||||
var password = _password ?? null;
|
||||
|
||||
return new TestSmtpSettings()
|
||||
{
|
||||
@@ -58,6 +84,9 @@ namespace Umbraco.Tests.Common.Builders
|
||||
Host = host,
|
||||
Port = port,
|
||||
PickupDirectoryLocation = pickupDirectoryLocation,
|
||||
DeliveryMethod = deliveryMethod,
|
||||
Username = username,
|
||||
Password = password,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +96,9 @@ namespace Umbraco.Tests.Common.Builders
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string PickupDirectoryLocation { get; set; }
|
||||
public SmtpDeliveryMethod DeliveryMethod { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace Umbraco.Tests.Common.Builders
|
||||
{
|
||||
public class XmlDocumentBuilder : BuilderBase<XmlDocument>
|
||||
{
|
||||
private string _content;
|
||||
|
||||
public XmlDocumentBuilder WithContent(string content)
|
||||
{
|
||||
_content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override XmlDocument Build()
|
||||
{
|
||||
var xml = new XmlDocument();
|
||||
xml.LoadXml(_content ?? DefaultContent);
|
||||
return xml;
|
||||
}
|
||||
|
||||
private const string DefaultContent =
|
||||
@"<?xml version=""1.0"" encoding=""utf-8""?>
|
||||
<!DOCTYPE root[
|
||||
<!ELEMENT Home ANY>
|
||||
<!ATTLIST Home id ID #REQUIRED>
|
||||
<!ELEMENT CustomDocument ANY>
|
||||
<!ATTLIST CustomDocument id ID #REQUIRED>
|
||||
]>
|
||||
<root id=""-1"">
|
||||
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""0"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
|
||||
<umbracoNaviHide>1</umbracoNaviHide>
|
||||
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""0"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""">
|
||||
<content><![CDATA[<div>This is some content</div>]]></content>
|
||||
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
|
||||
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""3"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
|
||||
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""2"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""1"" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
|
||||
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""1"" sortOrder=""0"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
|
||||
|
||||
<Home id=""1176"" parentID=""1179"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""26"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1180"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""25"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1181"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""24"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1182"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""23"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1183"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""22"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1184"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""21"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1185"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""20"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1186"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""19"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1187"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""18"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1188"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""17"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1189"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""16"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1190"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""15"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1191"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""14"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1192"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""13"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1193"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""12"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1194"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""11"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1195"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""10"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1196"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""9"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1197"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""8"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1198"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""7"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1199"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""6"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1200"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""5"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1201"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""4"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
</Home>
|
||||
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""1"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
|
||||
</Home>
|
||||
</Home>
|
||||
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""1 "" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
|
||||
</root>";
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Tests.Misc
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Core
|
||||
{
|
||||
[TestFixture]
|
||||
public class DateTimeExtensionsTests
|
||||
+5
-4
@@ -4,20 +4,21 @@ using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Tests.Misc
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Core
|
||||
{
|
||||
[TestFixture]
|
||||
public class HashCodeCombinerTests
|
||||
{
|
||||
|
||||
private DirectoryInfo PrepareFolder()
|
||||
{
|
||||
var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
|
||||
var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner", Guid.NewGuid().ToString("N")));
|
||||
var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner",
|
||||
Guid.NewGuid().ToString("N")));
|
||||
foreach (var f in dir.GetFiles())
|
||||
{
|
||||
f.Delete();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -80,6 +81,7 @@ namespace Umbraco.Tests.Misc
|
||||
{
|
||||
file1.WriteLine("hello");
|
||||
}
|
||||
|
||||
var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
|
||||
File.Delete(file2Path);
|
||||
using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
|
||||
@@ -140,6 +142,5 @@ namespace Umbraco.Tests.Misc
|
||||
|
||||
Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner3.GetCombinedHashCode());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -4,7 +4,7 @@ using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Tests.Misc
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Core
|
||||
{
|
||||
[TestFixture]
|
||||
public class HashGeneratorTests
|
||||
@@ -20,6 +20,7 @@ namespace Umbraco.Tests.Misc
|
||||
else
|
||||
generator.AddCaseInsensitiveString(str);
|
||||
}
|
||||
|
||||
return generator.GenerateHash();
|
||||
}
|
||||
}
|
||||
@@ -27,7 +28,6 @@ namespace Umbraco.Tests.Misc
|
||||
[Test]
|
||||
public void Generate_Hash_Multiple_Strings_Case_Sensitive()
|
||||
{
|
||||
|
||||
var hash1 = Generate(true, "hello", "world");
|
||||
var hash2 = Generate(true, "hello", "world");
|
||||
var hashFalse1 = Generate(true, "hello", "worlD");
|
||||
@@ -54,11 +54,13 @@ namespace Umbraco.Tests.Misc
|
||||
private DirectoryInfo PrepareFolder()
|
||||
{
|
||||
var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
|
||||
var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner", Guid.NewGuid().ToString("N")));
|
||||
var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner",
|
||||
Guid.NewGuid().ToString("N")));
|
||||
foreach (var f in dir.GetFiles())
|
||||
{
|
||||
f.Delete();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -74,7 +76,6 @@ namespace Umbraco.Tests.Misc
|
||||
combiner2.AddCaseInsensitiveString("world");
|
||||
Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -120,6 +121,7 @@ namespace Umbraco.Tests.Misc
|
||||
{
|
||||
file1.WriteLine("hello");
|
||||
}
|
||||
|
||||
var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
|
||||
File.Delete(file2Path);
|
||||
using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
|
||||
+41
-41
@@ -1,25 +1,18 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Tests.Misc
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Routing
|
||||
{
|
||||
// FIXME: not testing virtual directory!
|
||||
|
||||
[TestFixture]
|
||||
public class UriUtilityTests
|
||||
{
|
||||
|
||||
public UriUtility UriUtility { get; } = TestHelper.UriUtility;
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
SettingsForTests.Reset();
|
||||
}
|
||||
|
||||
// test normal urls
|
||||
[TestCase("http://LocalHost/", "http://localhost/")]
|
||||
[TestCase("http://LocalHost/?x=y", "http://localhost/?x=y")]
|
||||
@@ -50,19 +43,20 @@ namespace Umbraco.Tests.Misc
|
||||
[TestCase("http://Localhost/Home/Sub1.aspx/Sub2?x=y", "http://localhost/home/sub1/sub2?x=y")]
|
||||
[TestCase("http://Localhost/Home.aspx/Sub1.aspx/Sub2?x=y", "http://localhost/home/sub1/sub2?x=y")]
|
||||
[TestCase("http://Localhost/deFault.aspx/Home.aspx/deFault.aspx/Sub1.aspx", "http://localhost/home/default/sub1")]
|
||||
|
||||
public void Uri_To_Umbraco(string sourceUrl, string expectedUrl)
|
||||
{
|
||||
UriUtility.SetAppDomainAppVirtualPath("/");
|
||||
|
||||
var expectedUri = new Uri(expectedUrl);
|
||||
// Arrange
|
||||
var sourceUri = new Uri(sourceUrl);
|
||||
var resultUri = UriUtility.UriToUmbraco(sourceUri);
|
||||
var uriUtility = BuildUriUtility("/");
|
||||
|
||||
// Act
|
||||
var resultUri = uriUtility.UriToUmbraco(sourceUri);
|
||||
|
||||
// Assert
|
||||
var expectedUri = new Uri(expectedUrl);
|
||||
Assert.AreEqual(expectedUri.ToString(), resultUri.ToString());
|
||||
}
|
||||
|
||||
|
||||
// test directoryUrl true, trailingSlash false
|
||||
[TestCase("/", "/", false)]
|
||||
[TestCase("/home", "/home", false)]
|
||||
@@ -72,30 +66,22 @@ namespace Umbraco.Tests.Misc
|
||||
[TestCase("/", "/", true)]
|
||||
[TestCase("/home", "/home/", true)]
|
||||
[TestCase("/home/sub1", "/home/sub1/", true)]
|
||||
|
||||
public void Uri_From_Umbraco(string sourceUrl, string expectedUrl, bool trailingSlash)
|
||||
{
|
||||
var globalConfig = Mock.Get(SettingsForTests.GenerateMockGlobalSettings());
|
||||
// Arrange
|
||||
var sourceUri = new Uri(sourceUrl, UriKind.Relative);
|
||||
var mockRequestHandlerSettings = new Mock<IRequestHandlerSettings>();
|
||||
mockRequestHandlerSettings.Setup(x => x.AddTrailingSlash).Returns(trailingSlash);
|
||||
var uriUtility = BuildUriUtility("/");
|
||||
|
||||
var settings = SettingsForTests.GenerateMockRequestHandlerSettings();
|
||||
var requestMock = Mock.Get(settings);
|
||||
requestMock.Setup(x => x.AddTrailingSlash).Returns(trailingSlash);
|
||||
|
||||
UriUtility.SetAppDomainAppVirtualPath("/");
|
||||
|
||||
var expectedUri = NewUri(expectedUrl);
|
||||
var sourceUri = NewUri(sourceUrl);
|
||||
var resultUri = UriUtility.UriFromUmbraco(sourceUri, globalConfig.Object, settings);
|
||||
// Act
|
||||
var resultUri = uriUtility.UriFromUmbraco(sourceUri, Mock.Of<IGlobalSettings>(), mockRequestHandlerSettings.Object);
|
||||
|
||||
// Assert
|
||||
var expectedUri = new Uri(expectedUrl, UriKind.Relative);
|
||||
Assert.AreEqual(expectedUri.ToString(), resultUri.ToString());
|
||||
}
|
||||
|
||||
Uri NewUri(string url)
|
||||
{
|
||||
return new Uri(url, url.StartsWith("http:") ? UriKind.Absolute : UriKind.Relative);
|
||||
}
|
||||
|
||||
//
|
||||
[TestCase("/", "/", "/")]
|
||||
[TestCase("/", "/foo", "/foo")]
|
||||
[TestCase("/", "~/foo", "/foo")]
|
||||
@@ -103,15 +89,18 @@ namespace Umbraco.Tests.Misc
|
||||
[TestCase("/vdir", "/foo", "/vdir/foo")]
|
||||
[TestCase("/vdir", "/foo/", "/vdir/foo/")]
|
||||
[TestCase("/vdir", "~/foo", "/vdir/foo")]
|
||||
|
||||
public void Uri_To_Absolute(string virtualPath, string sourceUrl, string expectedUrl)
|
||||
{
|
||||
UriUtility.SetAppDomainAppVirtualPath(virtualPath);
|
||||
var resultUrl = UriUtility.ToAbsolute(sourceUrl);
|
||||
// Arrange
|
||||
var uriUtility = BuildUriUtility(virtualPath);
|
||||
|
||||
// Act
|
||||
var resultUrl = uriUtility.ToAbsolute(sourceUrl);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expectedUrl, resultUrl);
|
||||
}
|
||||
|
||||
//
|
||||
[TestCase("/", "/", "/")]
|
||||
[TestCase("/", "/foo", "/foo")]
|
||||
[TestCase("/", "/foo/", "/foo/")]
|
||||
@@ -119,12 +108,23 @@ namespace Umbraco.Tests.Misc
|
||||
[TestCase("/vdir", "/vdir/", "/")]
|
||||
[TestCase("/vdir", "/vdir/foo", "/foo")]
|
||||
[TestCase("/vdir", "/vdir/foo/", "/foo/")]
|
||||
|
||||
public void Url_To_App_Relative(string virtualPath, string sourceUrl, string expectedUrl)
|
||||
{
|
||||
UriUtility.SetAppDomainAppVirtualPath(virtualPath);
|
||||
var resultUrl = UriUtility.ToAppRelative(sourceUrl);
|
||||
// Arrange
|
||||
var uriUtility = BuildUriUtility(virtualPath);
|
||||
|
||||
// Act
|
||||
var resultUrl = uriUtility.ToAppRelative(sourceUrl);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expectedUrl, resultUrl);
|
||||
}
|
||||
|
||||
private UriUtility BuildUriUtility(string virtualPath)
|
||||
{
|
||||
var mockHostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
mockHostingEnvironment.Setup(x => x.ApplicationVirtualPath).Returns(virtualPath);
|
||||
return new UriUtility(mockHostingEnvironment.Object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.XPath;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Xml;
|
||||
using Umbraco.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Xml
|
||||
{
|
||||
[TestFixture]
|
||||
public class XmlHelperTests
|
||||
{
|
||||
private XmlDocumentBuilder _builder;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_builder = new XmlDocumentBuilder();
|
||||
}
|
||||
|
||||
[Ignore("This is a benchmark test so is ignored by default")]
|
||||
[Test]
|
||||
public void Sort_Nodes_Benchmark_Legacy()
|
||||
{
|
||||
var xml = _builder.Build();
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
long totalTime = 0;
|
||||
var watch = new Stopwatch();
|
||||
var iterations = 10000;
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
//don't measure the time for clone!
|
||||
var parentNode = original.Clone();
|
||||
watch.Start();
|
||||
LegacySortNodes(ref parentNode);
|
||||
watch.Stop();
|
||||
totalTime += watch.ElapsedMilliseconds;
|
||||
watch.Reset();
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
}
|
||||
|
||||
Debug.WriteLine("Total time for " + iterations + " iterations is " + totalTime);
|
||||
}
|
||||
|
||||
[Ignore("This is a benchmark test so is ignored by default")]
|
||||
[Test]
|
||||
public void Sort_Nodes_Benchmark_New()
|
||||
{
|
||||
var xml = _builder.Build();
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
long totalTime = 0;
|
||||
var watch = new Stopwatch();
|
||||
var iterations = 10000;
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
//don't measure the time for clone!
|
||||
var parentNode = (XmlElement) original.Clone();
|
||||
watch.Start();
|
||||
XmlHelper.SortNodes(
|
||||
parentNode,
|
||||
"./* [@id]",
|
||||
x => x.AttributeValue<int>("sortOrder"));
|
||||
watch.Stop();
|
||||
totalTime += watch.ElapsedMilliseconds;
|
||||
watch.Reset();
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
}
|
||||
|
||||
Debug.WriteLine("Total time for " + iterations + " iterations is " + totalTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sort_Nodes()
|
||||
{
|
||||
var xml = _builder.Build();
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
var parentNode = (XmlElement) original.Clone();
|
||||
|
||||
XmlHelper.SortNodes(
|
||||
parentNode,
|
||||
"./* [@id]",
|
||||
x => x.AttributeValue<int>("sortOrder"));
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This was the logic to sort before and now lives here just to show the benchmarks tests above.
|
||||
/// </summary>
|
||||
/// <param name="parentNode"></param>
|
||||
private static void LegacySortNodes(ref XmlNode parentNode)
|
||||
{
|
||||
var n = parentNode.CloneNode(true);
|
||||
|
||||
// remove all children from original node
|
||||
var xpath = "./* [@id]";
|
||||
foreach (XmlNode child in parentNode.SelectNodes(xpath))
|
||||
parentNode.RemoveChild(child);
|
||||
|
||||
var nav = n.CreateNavigator();
|
||||
var expr = nav.Compile(xpath);
|
||||
expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
|
||||
var iterator = nav.Select(expr);
|
||||
while (iterator.MoveNext())
|
||||
parentNode.AppendChild(
|
||||
((IHasXmlNode) iterator.Current).GetNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders
|
||||
{
|
||||
[TestFixture]
|
||||
public class XmlDocumentBuilderTests
|
||||
{
|
||||
[Test]
|
||||
public void Is_Built_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
const string content =
|
||||
@"<?xml version=""1.0"" encoding=""utf-8""?><root id=""-1""></root>";
|
||||
|
||||
var builder = new XmlDocumentBuilder();
|
||||
|
||||
// Act
|
||||
var xml = builder
|
||||
.WithContent(content)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(content, xml.OuterXml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.BackOffice.Filters;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
[TestFixture]
|
||||
public class AppendUserModifiedHeaderAttributeTests
|
||||
{
|
||||
[Test]
|
||||
public void Appends_Header_When_No_User_Parameter_Provider()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
var attribute = new AppendUserModifiedHeaderAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
context.HttpContext.Response.Headers.TryGetValue("X-Umb-User-Modified", out var headerValue);
|
||||
Assert.AreEqual("1", headerValue[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Append_Header_If_Already_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(headerValue: "0");
|
||||
var attribute = new AppendUserModifiedHeaderAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
context.HttpContext.Response.Headers.TryGetValue("X-Umb-User-Modified", out var headerValue);
|
||||
Assert.AreEqual("0", headerValue[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Append_Header_When_User_Id_Parameter_Provided_And_Does_Not_Match_Current_User()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(actionArgument: new KeyValuePair<string, object>("UserId", 99));
|
||||
var userIdParameter = "UserId";
|
||||
var attribute = new AppendUserModifiedHeaderAttribute(userIdParameter);
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(context.HttpContext.Response.Headers.ContainsKey("X-Umb-User-Modified"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Appends_Header_When_User_Id_Parameter_Provided_And_Does_Not_Match_Current_User()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(actionArgument: new KeyValuePair<string, object>("UserId", 100));
|
||||
var userIdParameter = "UserId";
|
||||
var attribute = new AppendUserModifiedHeaderAttribute(userIdParameter);
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
context.HttpContext.Response.Headers.TryGetValue("X-Umb-User-Modified", out var headerValue);
|
||||
Assert.AreEqual("1", headerValue[0]);
|
||||
}
|
||||
|
||||
private static ActionExecutingContext CreateContext(string headerValue = null, KeyValuePair<string, object> actionArgument = default)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
if (!string.IsNullOrEmpty(headerValue))
|
||||
{
|
||||
httpContext.Response.Headers.Add("X-Umb-User-Modified", headerValue);
|
||||
}
|
||||
|
||||
var currentUserMock = new Mock<IUser>();
|
||||
currentUserMock
|
||||
.SetupGet(x => x.Id)
|
||||
.Returns(100);
|
||||
|
||||
var webSecurityMock = new Mock<IWebSecurity>();
|
||||
webSecurityMock
|
||||
.SetupGet(x => x.CurrentUser)
|
||||
.Returns(currentUserMock.Object);
|
||||
|
||||
var umbracoContextMock = new Mock<IUmbracoContext>();
|
||||
umbracoContextMock
|
||||
.SetupGet(x => x.Security)
|
||||
.Returns(webSecurityMock.Object);
|
||||
|
||||
var umbracoContextAccessorMock = new Mock<IUmbracoContextAccessor>();
|
||||
umbracoContextAccessorMock
|
||||
.SetupGet(x => x.UmbracoContext)
|
||||
.Returns(umbracoContextMock.Object);
|
||||
|
||||
var serviceProviderMock = new Mock<IServiceProvider>();
|
||||
serviceProviderMock
|
||||
.Setup(x => x.GetService(typeof(IUmbracoContextAccessor)))
|
||||
.Returns(umbracoContextAccessorMock.Object);
|
||||
|
||||
httpContext.RequestServices = serviceProviderMock.Object;
|
||||
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
var context = new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new Mock<Controller>().Object);
|
||||
|
||||
if (!EqualityComparer<KeyValuePair<string, object>>.Default.Equals(actionArgument, default))
|
||||
{
|
||||
context.ActionArguments.Add(actionArgument);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.BackOffice.Filters;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
[TestFixture]
|
||||
public class OnlyLocalRequestsAttributeTests
|
||||
{
|
||||
[Test]
|
||||
public void Does_Not_Set_Result_When_No_Remote_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(context.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Set_Result_When_Remote_Address_Is_Null_Ip_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(remoteIpAddress: "::1");
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(context.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Set_Result_When_Remote_Address_Matches_Local_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.3");
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(context.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_Local_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.2");
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
var typedResult = context.Result as NotFoundResult;
|
||||
Assert.IsNotNull(typedResult);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Set_Result_When_Remote_Address_Matches_LoopBack_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(remoteIpAddress: "127.0.0.1", localIpAddress: "::1");
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(context.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_LoopBack_Address()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "::1");
|
||||
var attribute = new OnlyLocalRequestsAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
var typedResult = context.Result as NotFoundResult;
|
||||
Assert.IsNotNull(typedResult);
|
||||
}
|
||||
|
||||
private static ActionExecutingContext CreateContext(string remoteIpAddress = null, string localIpAddress = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
if (!string.IsNullOrEmpty(remoteIpAddress))
|
||||
{
|
||||
httpContext.Connection.RemoteIpAddress = IPAddress.Parse(remoteIpAddress);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(localIpAddress))
|
||||
{
|
||||
httpContext.Connection.LocalIpAddress = IPAddress.Parse(localIpAddress);
|
||||
}
|
||||
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
return new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new Mock<Controller>().Object);
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.BackOffice.Filters;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
[TestFixture]
|
||||
public class ValidationFilterAttributeTests
|
||||
{
|
||||
[Test]
|
||||
public void Does_Not_Set_Result_When_No_Errors_In_Model_State()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
var attribute = new ValidationFilterAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(context.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Bad_Request_When_Errors_In_Model_State()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(withError: true);
|
||||
var attribute = new ValidationFilterAttribute();
|
||||
|
||||
// Act
|
||||
attribute.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
var typedResult = context.Result as BadRequestObjectResult;
|
||||
Assert.IsNotNull(typedResult);
|
||||
}
|
||||
|
||||
private static ActionExecutingContext CreateContext(bool withError = false)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
if (withError)
|
||||
{
|
||||
modelState.AddModelError(string.Empty, "Error");
|
||||
}
|
||||
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), modelState);
|
||||
|
||||
return new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new Mock<Controller>().Object);
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Common.ModelBinders;
|
||||
using Umbraco.Web.Models;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
[TestFixture]
|
||||
public class ContentModelBinderTests
|
||||
{
|
||||
[Test]
|
||||
public void Does_Not_Bind_Model_When_UmbracoDataToken_Not_In_Route_Data()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext(typeof(ContentModel), withUmbracoDataToken: false);
|
||||
var binder = new ContentModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Bind_Model_When_Source_Not_Of_Expected_Type()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext(typeof(ContentModel), source: new NonContentModel());
|
||||
var binder = new ContentModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Does_Not_Bind_Model_When_Source_Type_Matches_Model_Type()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext(typeof(ContentModel), source: new ContentModel(CreatePublishedContent()));
|
||||
var binder = new ContentModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Binds_From_IPublishedContent_To_Content_Model()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext(typeof(ContentModel), source: CreatePublishedContent());
|
||||
var binder = new ContentModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Binds_From_IPublishedContent_To_Content_Model_Of_T()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext(typeof(ContentModel<ContentType1>), source: new ContentModel<ContentType2>(new ContentType2(CreatePublishedContent())));
|
||||
var binder = new ContentModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
private ModelBindingContext CreateBindingContext(Type modelType, bool withUmbracoDataToken = true, object source = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var routeData = new RouteData();
|
||||
if (withUmbracoDataToken)
|
||||
routeData.DataTokens.Add(Constants.Web.UmbracoDataToken, source);
|
||||
|
||||
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var routeValueDictionary = new RouteValueDictionary();
|
||||
var valueProvider = new RouteValueProvider(BindingSource.Path, routeValueDictionary);
|
||||
return new DefaultModelBindingContext
|
||||
{
|
||||
ActionContext = actionContext,
|
||||
ModelMetadata = metadataProvider.GetMetadataForType(modelType),
|
||||
ModelName = modelType.Name,
|
||||
ValueProvider = valueProvider,
|
||||
};
|
||||
}
|
||||
|
||||
private class NonContentModel
|
||||
{
|
||||
}
|
||||
|
||||
private IPublishedContent CreatePublishedContent()
|
||||
{
|
||||
return new ContentType2(new Mock<IPublishedContent>().Object);
|
||||
}
|
||||
|
||||
public class ContentType1 : PublishedContentWrapped
|
||||
{
|
||||
public ContentType1(IPublishedContent content) : base(content) { }
|
||||
}
|
||||
|
||||
public class ContentType2 : ContentType1
|
||||
{
|
||||
public ContentType2(IPublishedContent content) : base(content) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Common.ModelBinders;
|
||||
|
||||
namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
[TestFixture]
|
||||
public class HttpQueryStringModelBinderTests
|
||||
{
|
||||
[Test]
|
||||
public void Binds_Query_To_FormCollection()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext("?foo=bar&baz=buzz");
|
||||
var binder = new HttpQueryStringModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var typedModel = bindingContext.Result.Model as FormCollection;
|
||||
Assert.IsNotNull(typedModel);
|
||||
Assert.AreEqual(typedModel["foo"], "bar");
|
||||
Assert.AreEqual(typedModel["baz"], "buzz");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sets_Culture_Form_Value_From_Query_If_Provided()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext("?foo=bar&baz=buzz&culture=en-gb");
|
||||
var binder = new HttpQueryStringModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var typedModel = bindingContext.Result.Model as FormCollection;
|
||||
Assert.IsNotNull(typedModel);
|
||||
Assert.AreEqual(typedModel["culture"], "en-gb");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sets_Culture_Form_Value_From_Header_If_Not_Provided_In_Query()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateBindingContext("?foo=bar&baz=buzz");
|
||||
var binder = new HttpQueryStringModelBinder();
|
||||
|
||||
// Act
|
||||
binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var typedModel = bindingContext.Result.Model as FormCollection;
|
||||
Assert.IsNotNull(typedModel);
|
||||
Assert.AreEqual(typedModel["culture"], "en-gb");
|
||||
}
|
||||
|
||||
private ModelBindingContext CreateBindingContext(string querystring)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new QueryString(querystring);
|
||||
httpContext.Request.Headers.Add("X-UMB-CULTURE", new StringValues("en-gb"));
|
||||
var routeData = new RouteData();
|
||||
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var routeValueDictionary = new RouteValueDictionary();
|
||||
var valueProvider = new RouteValueProvider(BindingSource.Path, routeValueDictionary);
|
||||
var modelType = typeof(FormCollection);
|
||||
return new DefaultModelBindingContext
|
||||
{
|
||||
ActionContext = actionContext,
|
||||
ModelMetadata = metadataProvider.GetMetadataForType(modelType),
|
||||
ModelName = modelType.Name,
|
||||
ValueProvider = valueProvider,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +333,24 @@ namespace Umbraco.Tests.Composing
|
||||
Assert.AreSame(s1, s2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanRegisterMultipleSameTypeParametersWithCreateInstance()
|
||||
{
|
||||
var register = GetRegister();
|
||||
|
||||
register.Register<Thing4>(c =>
|
||||
{
|
||||
const string param1 = "param1";
|
||||
const string param2 = "param2";
|
||||
|
||||
return c.CreateInstance<Thing4>(param1, param2);
|
||||
});
|
||||
|
||||
var factory = register.CreateFactory();
|
||||
var instance = factory.GetInstance<Thing4>();
|
||||
Assert.AreNotEqual(instance.Thing, instance.AnotherThing);
|
||||
}
|
||||
|
||||
public interface IThing { }
|
||||
|
||||
public abstract class ThingBase : IThing { }
|
||||
@@ -353,5 +371,17 @@ namespace Umbraco.Tests.Composing
|
||||
|
||||
public IEnumerable<ThingBase> Things { get; }
|
||||
}
|
||||
|
||||
public class Thing4 : ThingBase
|
||||
{
|
||||
public readonly string Thing;
|
||||
public readonly string AnotherThing;
|
||||
|
||||
public Thing4(string thing, string anotherThing)
|
||||
{
|
||||
Thing = thing;
|
||||
AnotherThing = anotherThing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Tests.CoreThings
|
||||
{
|
||||
public class ClaimsIdentityExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void FindFirstValue_WhenIdentityIsNull_ExpectArgumentNullException()
|
||||
{
|
||||
ClaimsIdentity identity = null;
|
||||
Assert.Throws<ArgumentNullException>(() => identity.FindFirstValue("test"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenClaimNotPresent_ExpectNull()
|
||||
{
|
||||
var identity = new ClaimsIdentity(new List<Claim>());
|
||||
var value = identity.FindFirstValue("test");
|
||||
Assert.IsNull(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenMatchingClaimPresent_ExpectCorrectValue()
|
||||
{
|
||||
var expectedClaim = new Claim("test", "123", "string", "Umbraco");
|
||||
var identity = new ClaimsIdentity(new List<Claim> {expectedClaim});
|
||||
|
||||
var value = identity.FindFirstValue("test");
|
||||
|
||||
Assert.AreEqual(expectedClaim.Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenMultipleMatchingClaimsPresent_ExpectFirstValue()
|
||||
{
|
||||
var expectedClaim = new Claim("test", "123", "string", "Umbraco");
|
||||
var dupeClaim = new Claim(expectedClaim.Type, Guid.NewGuid().ToString());
|
||||
var identity = new ClaimsIdentity(new List<Claim> {expectedClaim, dupeClaim});
|
||||
|
||||
var value = identity.FindFirstValue("test");
|
||||
|
||||
Assert.AreEqual(expectedClaim.Value, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.XPath;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Xml;
|
||||
|
||||
namespace Umbraco.Tests.Misc
|
||||
{
|
||||
[TestFixture]
|
||||
public class XmlHelperTests
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[Ignore("This is a benchmark test so is ignored by default")]
|
||||
[Test]
|
||||
public void Sort_Nodes_Benchmark_Legacy()
|
||||
{
|
||||
var xmlContent = GetXmlContent(1);
|
||||
var xml = new XmlDocument();
|
||||
xml.LoadXml(xmlContent);
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
long totalTime = 0;
|
||||
var watch = new Stopwatch();
|
||||
var iterations = 10000;
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
//don't measure the time for clone!
|
||||
var parentNode = original.Clone();
|
||||
watch.Start();
|
||||
LegacySortNodes(ref parentNode);
|
||||
watch.Stop();
|
||||
totalTime += watch.ElapsedMilliseconds;
|
||||
watch.Reset();
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
}
|
||||
|
||||
Debug.WriteLine("Total time for " + iterations + " iterations is " + totalTime);
|
||||
}
|
||||
|
||||
[Ignore("This is a benchmark test so is ignored by default")]
|
||||
[Test]
|
||||
public void Sort_Nodes_Benchmark_New()
|
||||
{
|
||||
var xmlContent = GetXmlContent(1);
|
||||
var xml = new XmlDocument();
|
||||
xml.LoadXml(xmlContent);
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
long totalTime = 0;
|
||||
var watch = new Stopwatch();
|
||||
var iterations = 10000;
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
//don't measure the time for clone!
|
||||
var parentNode = (XmlElement)original.Clone();
|
||||
watch.Start();
|
||||
XmlHelper.SortNodes(
|
||||
parentNode,
|
||||
"./* [@id]",
|
||||
x => x.AttributeValue<int>("sortOrder"));
|
||||
watch.Stop();
|
||||
totalTime += watch.ElapsedMilliseconds;
|
||||
watch.Reset();
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
|
||||
}
|
||||
|
||||
Debug.WriteLine("Total time for " + iterations + " iterations is " + totalTime);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void Sort_Nodes()
|
||||
{
|
||||
var xmlContent = GetXmlContent(1);
|
||||
var xml = new XmlDocument();
|
||||
xml.LoadXml(xmlContent);
|
||||
var original = xml.GetElementById(1173.ToString());
|
||||
Assert.IsNotNull(original);
|
||||
|
||||
var parentNode = (XmlElement)original.Clone();
|
||||
|
||||
XmlHelper.SortNodes(
|
||||
parentNode,
|
||||
"./* [@id]",
|
||||
x => x.AttributeValue<int>("sortOrder"));
|
||||
|
||||
//do assertions just to make sure it is working properly.
|
||||
var currSort = 0;
|
||||
foreach (var child in parentNode.SelectNodes("./* [@id]").Cast<XmlNode>())
|
||||
{
|
||||
Assert.AreEqual(currSort, int.Parse(child.Attributes["sortOrder"].Value));
|
||||
currSort++;
|
||||
}
|
||||
//ensure the parent node's properties still exist first
|
||||
Assert.AreEqual("content", parentNode.ChildNodes[0].Name);
|
||||
Assert.AreEqual("umbracoUrlAlias", parentNode.ChildNodes[1].Name);
|
||||
//then the child nodes should come straight after
|
||||
Assert.IsTrue(parentNode.ChildNodes[2].Attributes["id"] != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This was the logic to sort before and now lives here just to show the benchmarks tests above.
|
||||
/// </summary>
|
||||
/// <param name="parentNode"></param>
|
||||
private static void LegacySortNodes(ref XmlNode parentNode)
|
||||
{
|
||||
XmlNode n = parentNode.CloneNode(true);
|
||||
|
||||
// remove all children from original node
|
||||
string xpath = "./* [@id]";
|
||||
foreach (XmlNode child in parentNode.SelectNodes(xpath))
|
||||
parentNode.RemoveChild(child);
|
||||
|
||||
|
||||
XPathNavigator nav = n.CreateNavigator();
|
||||
XPathExpression expr = nav.Compile(xpath);
|
||||
expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
|
||||
XPathNodeIterator iterator = nav.Select(expr);
|
||||
while (iterator.MoveNext())
|
||||
parentNode.AppendChild(
|
||||
((IHasXmlNode)iterator.Current).GetNode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns xml with a reverse sort order
|
||||
/// </summary>
|
||||
/// <param name="templateId"></param>
|
||||
/// <returns></returns>
|
||||
private string GetXmlContent(int templateId)
|
||||
{
|
||||
return @"<?xml version=""1.0"" encoding=""utf-8""?>
|
||||
<!DOCTYPE root[
|
||||
<!ELEMENT Home ANY>
|
||||
<!ATTLIST Home id ID #REQUIRED>
|
||||
<!ELEMENT CustomDocument ANY>
|
||||
<!ATTLIST CustomDocument id ID #REQUIRED>
|
||||
]>
|
||||
<root id=""-1"">
|
||||
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""0"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
|
||||
<umbracoNaviHide>1</umbracoNaviHide>
|
||||
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""0"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""">
|
||||
<content><![CDATA[<div>This is some content</div>]]></content>
|
||||
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
|
||||
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
|
||||
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
|
||||
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""0"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
|
||||
|
||||
<Home id=""1176"" parentID=""1179"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""26"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1180"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""25"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1181"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""24"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1182"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""23"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1183"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""22"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1184"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""21"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1185"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""20"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1186"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""19"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1187"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""18"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1188"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""17"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1189"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""16"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1190"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""15"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1191"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""14"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1192"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""13"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1193"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""12"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1194"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""11"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1195"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""10"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1196"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""9"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1197"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""8"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1198"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""7"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1199"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""6"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1200"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""5"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
<Home id=""1176"" parentID=""1201"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
|
||||
<content><![CDATA[]]></content>
|
||||
</Home>
|
||||
</Home>
|
||||
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
|
||||
</Home>
|
||||
</Home>
|
||||
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
|
||||
</root>";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class AuthenticationExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Errors_Are_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
IEnumerable<IdentityError> errors = null;
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Single_Error_Expect_Error_Description()
|
||||
{
|
||||
const string expectedError = "invalid something";
|
||||
var errors = new List<IdentityError> {new IdentityError {Code = "1", Description = expectedError}};
|
||||
|
||||
var errorMessage = errors.ToErrorMessage();
|
||||
|
||||
Assert.AreEqual(expectedError, errorMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Multiple_Errors_Expect_Error_Descriptions_With_Comma_Delimiter()
|
||||
{
|
||||
const string error1 = "invalid something";
|
||||
const string error2 = "invalid something else";
|
||||
var errors = new List<IdentityError>
|
||||
{
|
||||
new IdentityError {Code = "1", Description = error1},
|
||||
new IdentityError {Code = "2", Description = error2}
|
||||
};
|
||||
|
||||
var errorMessage = errors.ToErrorMessage();
|
||||
|
||||
Assert.AreEqual($"{error1}, {error2}", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
[TestFixture]
|
||||
public class BackOfficeClaimsPrincipalFactoryTests
|
||||
{
|
||||
private BackOfficeIdentityUser _testUser;
|
||||
private Mock<UserManager<BackOfficeIdentityUser>> _mockUserManager;
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
null,
|
||||
new OptionsWrapper<IdentityOptions>(new IdentityOptions())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Options_Are_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null).Object,
|
||||
null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Options_Value_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null).Object,
|
||||
new OptionsWrapper<IdentityOptions>(null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.CreateAsync(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_Principal_With_Umbraco_Identity()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
var umbracoBackOfficeIdentity = claimsPrincipal.Identity as UmbracoBackOfficeIdentity;
|
||||
Assert.IsNotNull(umbracoBackOfficeIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_NameId()
|
||||
{
|
||||
const string expectedClaimType = ClaimTypes.NameIdentifier;
|
||||
var expectedClaimValue = _testUser.Id.ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_Name()
|
||||
{
|
||||
const string expectedClaimType = ClaimTypes.Name;
|
||||
var expectedClaimValue = _testUser.UserName;
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_IdentityProvider()
|
||||
{
|
||||
const string expectedClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
|
||||
const string expectedClaimValue = "ASP.NET Identity";
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_When_SecurityStamp_Supported_Expect_SecurityStamp_Claim()
|
||||
{
|
||||
const string expectedClaimType = Constants.Web.SecurityStampClaimType;
|
||||
var expectedClaimValue = _testUser.SecurityStamp;
|
||||
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_When_Roles_Supported_Expect_Role_Claims_In_UmbracoIdentity()
|
||||
{
|
||||
const string expectedClaimType = ClaimTypes.Role;
|
||||
const string expectedClaimValue = "b87309fb-4caf-48dc-b45a-2b752d051508";
|
||||
|
||||
_testUser.Roles.Add(new Core.Models.Identity.IdentityUserRole<string>{RoleId = expectedClaimValue});
|
||||
_mockUserManager.Setup(x => x.SupportsUserRole).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetRolesAsync(_testUser)).ReturnsAsync(new[] {expectedClaimValue});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_When_UserClaims_Supported_Expect_UserClaims_In_Actor()
|
||||
{
|
||||
const string expectedClaimType = "custom";
|
||||
const string expectedClaimValue = "val";
|
||||
|
||||
_testUser.Claims.Add(new Core.Models.Identity.IdentityUserClaim<int> {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue});
|
||||
_mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync(
|
||||
new List<Claim> {new Claim(expectedClaimType, expectedClaimValue)});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "bob",
|
||||
Name = "Bob",
|
||||
Email = "bob@umbraco.test",
|
||||
SecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"
|
||||
};
|
||||
|
||||
_mockUserManager = new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null);
|
||||
_mockUserManager.Setup(x => x.GetUserIdAsync(_testUser)).ReturnsAsync(_testUser.Id.ToString);
|
||||
_mockUserManager.Setup(x => x.GetUserNameAsync(_testUser)).ReturnsAsync(_testUser.UserName);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
_mockUserManager.Setup(x => x.SupportsUserClaim).Returns(false);
|
||||
_mockUserManager.Setup(x => x.SupportsUserRole).Returns(false);
|
||||
}
|
||||
|
||||
private BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser> CreateSut()
|
||||
{
|
||||
return new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(_mockUserManager.Object,
|
||||
new OptionsWrapper<IdentityOptions>(new IdentityOptions()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class BackOfficeUserManagerTests
|
||||
{
|
||||
[Test]
|
||||
public async Task CheckPasswordAsync_When_Default_Password_Hasher_Validates_Umbraco7_Hash_Expect_Valid_Password()
|
||||
{
|
||||
const string v7Hash = "7Uob6fMTTxDIhWGebYiSxg==P+hgvWlXLbDd4cFLADn811KOaVI/9pg1PNvTuG5NklY=";
|
||||
const string plaintext = "4XxzH3s3&J";
|
||||
|
||||
var mockPasswordConfiguration = new Mock<IPasswordConfiguration>();
|
||||
var mockIpResolver = new Mock<IIpResolver>();
|
||||
var mockUserStore = new Mock<IUserPasswordStore<BackOfficeIdentityUser>>();
|
||||
var mockDataProtectionProvider = new Mock<IDataProtectionProvider>();
|
||||
|
||||
mockDataProtectionProvider.Setup(x => x.Create(It.IsAny<string>()))
|
||||
.Returns(new Mock<IDataProtector>().Object);
|
||||
mockPasswordConfiguration.Setup(x => x.HashAlgorithmType)
|
||||
.Returns("HMACSHA256");
|
||||
|
||||
var userManager = BackOfficeUserManager.Create(
|
||||
mockPasswordConfiguration.Object,
|
||||
mockIpResolver.Object,
|
||||
mockUserStore.Object,
|
||||
null,
|
||||
mockDataProtectionProvider.Object,
|
||||
new NullLogger<UserManager<BackOfficeIdentityUser>>());
|
||||
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
var user = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "alice",
|
||||
Name = "Alice",
|
||||
Email = "alice@umbraco.test",
|
||||
PasswordHash = v7Hash
|
||||
};
|
||||
|
||||
mockUserStore.Setup(x => x.GetPasswordHashAsync(user, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(v7Hash);
|
||||
|
||||
var isValidPassword = await userManager.CheckPasswordAsync(user, plaintext);
|
||||
|
||||
Assert.True(isValidPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class NopLookupNormalizerTests
|
||||
{
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
|
||||
{
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedName = sut.NormalizeName(name);
|
||||
|
||||
Assert.AreEqual(name, normalizedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeName_Expect_Input_Returned()
|
||||
{
|
||||
var name = Guid.NewGuid().ToString();
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedName = sut.NormalizeName(name);
|
||||
|
||||
Assert.AreEqual(name, normalizedName);
|
||||
}
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
|
||||
{
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedEmail = sut.NormalizeEmail(email);
|
||||
|
||||
Assert.AreEqual(email, normalizedEmail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeEmail_Expect_Input_Returned()
|
||||
{
|
||||
var email = $"{Guid.NewGuid()}@umbraco";
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedEmail = sut.NormalizeEmail(email);
|
||||
|
||||
Assert.AreEqual(email, normalizedEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class OwinDataProtectorTokenProviderTests
|
||||
{
|
||||
private Mock<IDataProtector> _mockDataProtector;
|
||||
private Mock<UserManager<BackOfficeIdentityUser>> _mockUserManager;
|
||||
private BackOfficeIdentityUser _testUser;
|
||||
private const string _testPurpose = "test";
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Protector_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new OwinDataProtectorTokenProvider<BackOfficeIdentityUser>(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var canGenerate = await sut.CanGenerateTwoFactorTokenAsync(_mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(canGenerate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAsync_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.GenerateAsync(null, null, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.GenerateAsync(null, _mockUserManager.Object, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_Expect_Ticks_And_User_ID()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var token = await sut.GenerateAsync(null, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
|
||||
var foundUserId = reader.ReadString();
|
||||
|
||||
Assert.That(creationTime.DateTime, Is.EqualTo(DateTime.UtcNow).Within(1).Minutes);
|
||||
Assert.AreEqual(_testUser.Id.ToString(), foundUserId);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_With_Purpose_Expect_Purpose_In_Token()
|
||||
{
|
||||
var expectedPurpose = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var token = await sut.GenerateAsync(expectedPurpose, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
reader.ReadInt64(); // creation time
|
||||
reader.ReadString(); // user ID
|
||||
var purpose = reader.ReadString();
|
||||
|
||||
Assert.AreEqual(expectedPurpose, purpose);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_And_SecurityStamp_Supported_Expect_SecurityStamp_In_Token()
|
||||
{
|
||||
var expectedSecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(expectedSecurityStamp);
|
||||
|
||||
var token = await sut.GenerateAsync(null, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
reader.ReadInt64(); // creation time
|
||||
reader.ReadString(); // user ID
|
||||
reader.ReadString(); // purpose
|
||||
var securityStamp = reader.ReadString();
|
||||
|
||||
Assert.AreEqual(expectedSecurityStamp, securityStamp);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void ValidateAsync_When_Token_Is_Null_Or_Whitespace_Expect_ArgumentNullException(string token)
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, token, _mockUserManager.Object, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateAsync_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, Guid.NewGuid().ToString(), null, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, Guid.NewGuid().ToString(), _mockUserManager.Object, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Has_Expired_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(creationDate: DateTime.UtcNow.AddYears(-10));
|
||||
|
||||
var isValid = await sut.ValidateAsync(null, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Issued_To_Wrong_User_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(userId: Guid.NewGuid().ToString());
|
||||
|
||||
var isValid = await sut.ValidateAsync(_testPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Has_Wrong_Purpose_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(purpose: "invalid");
|
||||
|
||||
var isValid = await sut.ValidateAsync("valid", testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Has_Wrong_SecurityStamp_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(Guid.NewGuid().ToString);
|
||||
|
||||
var testToken = CreateTestToken(securityStamp: "invalid");
|
||||
|
||||
var isValid = await sut.ValidateAsync(_testPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Valid_Token_Expect_True()
|
||||
{
|
||||
const string validPurpose = "test";
|
||||
var validSecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(validSecurityStamp);
|
||||
|
||||
var testToken = CreateTestToken(
|
||||
creationDate: DateTime.UtcNow,
|
||||
userId: _testUser.Id.ToString(),
|
||||
purpose: validPurpose,
|
||||
securityStamp: validSecurityStamp);
|
||||
|
||||
var isValid = await sut.ValidateAsync(validPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.True(isValid);
|
||||
}
|
||||
|
||||
private OwinDataProtectorTokenProvider<BackOfficeIdentityUser> CreateSut()
|
||||
=> new OwinDataProtectorTokenProvider<BackOfficeIdentityUser>(_mockDataProtector.Object);
|
||||
|
||||
private string CreateTestToken(DateTime? creationDate = null, string userId = null, string purpose = null, string securityStamp = null)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(ms))
|
||||
{
|
||||
writer.Write(creationDate?.Ticks ?? DateTimeOffset.UtcNow.UtcTicks);
|
||||
writer.Write(userId ?? _testUser.Id.ToString());
|
||||
writer.Write(purpose ?? _testPurpose);
|
||||
writer.Write(securityStamp ?? "");
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_mockDataProtector = new Mock<IDataProtector>();
|
||||
_mockDataProtector.Setup(x => x.Protect(It.IsAny<byte[]>())).Returns((byte[] originalBytes) => originalBytes);
|
||||
_mockDataProtector.Setup(x => x.Unprotect(It.IsAny<byte[]>())).Returns((byte[] originalBytes) => originalBytes);
|
||||
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
_mockUserManager = new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
|
||||
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "alice",
|
||||
Name = "Alice",
|
||||
Email = "alice@umbraco.test",
|
||||
SecurityStamp = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Umbraco.Tests.Security
|
||||
new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
new Claim(Constants.Security.SessionIdClaimType, sessionId, Constants.Security.SessionIdClaimType, TestIssuer, TestIssuer),
|
||||
new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
});
|
||||
|
||||
var backofficeIdentity = UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Logging;
|
||||
using Microsoft.Owin.Security;
|
||||
using Microsoft.Owin.Security.Cookies;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class UmbracoSecurityStampValidatorTests
|
||||
{
|
||||
private Mock<IOwinContext> _mockOwinContext;
|
||||
private Mock<BackOfficeUserManager<BackOfficeIdentityUser>> _mockUserManager;
|
||||
private Mock<BackOfficeSignInManager> _mockSignInManager;
|
||||
|
||||
private AuthenticationTicket _testAuthTicket;
|
||||
private CookieAuthenticationOptions _testOptions;
|
||||
private BackOfficeIdentityUser _testUser;
|
||||
private const string _testAuthType = "cookie";
|
||||
|
||||
[Test]
|
||||
public void OnValidateIdentity_When_GetUserIdCallback_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MaxValue, null, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op()
|
||||
{
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MaxValue, null, identity => throw new Exception());
|
||||
|
||||
_testAuthTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.AreEqual(_testAuthTicket.Identity, context.Identity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnValidateIdentity_When_Time_To_Validate_But_No_UserManager_Expect_InvalidOperationException()
|
||||
{
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => throw new Exception());
|
||||
|
||||
_mockOwinContext.Setup(x => x.Get<BackOfficeUserManager<BackOfficeIdentityUser>>(It.IsAny<string>()))
|
||||
.Returns((BackOfficeUserManager<BackOfficeIdentityUser>) null);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await func(context));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException()
|
||||
{
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => throw new Exception());
|
||||
|
||||
_mockOwinContext.Setup(x => x.Get<BackOfficeSignInManager>(It.IsAny<string>()))
|
||||
.Returns((BackOfficeSignInManager) null);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await func(context));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Time_To_Validate_And_User_No_Longer_Found_Expect_Rejected()
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => userId);
|
||||
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(userId))
|
||||
.ReturnsAsync((BackOfficeIdentityUser) null);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.IsNull(context.Identity);
|
||||
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Time_To_Validate_And_User_Exists_And_Does_Not_Support_SecurityStamps_Expect_Rejected()
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => userId);
|
||||
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.IsNull(context.Identity);
|
||||
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Time_To_Validate_And_SecurityStamp_Has_Changed_Expect_Rejected()
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => userId);
|
||||
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(Guid.NewGuid().ToString);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.IsNull(context.Identity);
|
||||
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Time_To_Validate_And_SecurityStamp_Has_Not_Changed_Expect_No_Change()
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => userId);
|
||||
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.AreEqual(_testAuthTicket.Identity, context.Identity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_User_Validated_And_RegenerateIdentityCallback_Present_Expect_User_Refreshed()
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
var expectedIdentity = new ClaimsIdentity(new List<Claim> {new Claim("sub", "bob")});
|
||||
|
||||
var regenFuncCalled = false;
|
||||
Func<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser, Task<ClaimsIdentity>> regenFunc =
|
||||
(signInManager, userManager, user) =>
|
||||
{
|
||||
regenFuncCalled = true;
|
||||
return Task.FromResult(expectedIdentity);
|
||||
};
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, regenFunc, identity => userId);
|
||||
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
|
||||
|
||||
var context = new CookieValidateIdentityContext(
|
||||
_mockOwinContext.Object,
|
||||
_testAuthTicket,
|
||||
_testOptions);
|
||||
|
||||
ClaimsIdentity callbackIdentity = null;
|
||||
_mockOwinContext.Setup(x => x.Authentication.SignIn(context.Properties, It.IsAny<ClaimsIdentity>()))
|
||||
.Callback((AuthenticationProperties props, ClaimsIdentity[] identities) => callbackIdentity = identities.FirstOrDefault())
|
||||
.Verifiable();
|
||||
|
||||
await func(context);
|
||||
|
||||
Assert.True(regenFuncCalled);
|
||||
Assert.AreEqual(expectedIdentity, callbackIdentity);
|
||||
Assert.IsNull(context.Properties.IssuedUtc);
|
||||
Assert.IsNull(context.Properties.ExpiresUtc);
|
||||
|
||||
_mockOwinContext.Verify();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "alice",
|
||||
Name = "Alice",
|
||||
Email = "alice@umbraco.test",
|
||||
SecurityStamp = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
_testAuthTicket = new AuthenticationTicket(
|
||||
new ClaimsIdentity(
|
||||
new List<Claim> {new Claim("sub", "alice"), new Claim(Constants.Web.SecurityStampClaimType, _testUser.SecurityStamp)},
|
||||
_testAuthType),
|
||||
new AuthenticationProperties());
|
||||
_testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType };
|
||||
|
||||
_mockUserManager = new Mock<BackOfficeUserManager<BackOfficeIdentityUser>>(
|
||||
new Mock<IPasswordConfiguration>().Object,
|
||||
new Mock<IIpResolver>().Object,
|
||||
new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null);
|
||||
_mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>())).ReturnsAsync((BackOfficeIdentityUser) null);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
|
||||
_mockSignInManager = new Mock<BackOfficeSignInManager>(
|
||||
_mockUserManager.Object,
|
||||
new Mock<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>>().Object,
|
||||
new Mock<IAuthenticationManager>().Object,
|
||||
new Mock<ILogger>().Object,
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
new Mock<IOwinRequest>().Object);
|
||||
|
||||
_mockOwinContext = new Mock<IOwinContext>();
|
||||
_mockOwinContext.Setup(x => x.Get<BackOfficeUserManager<BackOfficeIdentityUser>>(It.IsAny<string>()))
|
||||
.Returns(_mockUserManager.Object);
|
||||
_mockOwinContext.Setup(x => x.Get<BackOfficeSignInManager>(It.IsAny<string>()))
|
||||
.Returns(_mockSignInManager.Object);
|
||||
|
||||
_mockOwinContext.Setup(x => x.Authentication.SignOut(It.IsAny<string>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,8 +99,8 @@ namespace Umbraco.Tests.Services
|
||||
private void AssertJsonStartsWith(int id, string expected)
|
||||
{
|
||||
var json = GetJson(id).Replace('"', '\'');
|
||||
var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase);
|
||||
json = json.Substring(0, pos + "'cultureData':".Length);
|
||||
var pos = json.IndexOf("'cd':", StringComparison.InvariantCultureIgnoreCase);
|
||||
json = json.Substring(0, pos + "'cd':".Length);
|
||||
Assert.AreEqual(expected, json);
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch content type to Nothing
|
||||
contentType.Variations = ContentVariation.Nothing;
|
||||
@@ -623,7 +623,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch content back to Culture
|
||||
contentType.Variations = ContentVariation.Culture;
|
||||
@@ -640,7 +640,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch property back to Culture
|
||||
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture;
|
||||
@@ -656,7 +656,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -697,7 +697,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch content type to Culture
|
||||
contentType.Variations = ContentVariation.Culture;
|
||||
@@ -713,7 +713,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch property to Culture
|
||||
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture;
|
||||
@@ -728,7 +728,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch content back to Nothing
|
||||
contentType.Variations = ContentVariation.Nothing;
|
||||
@@ -745,7 +745,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -783,7 +783,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch property type to Nothing
|
||||
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing;
|
||||
@@ -800,7 +800,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch property back to Culture
|
||||
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture;
|
||||
@@ -816,7 +816,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':");
|
||||
|
||||
// switch other property to Culture
|
||||
contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture;
|
||||
@@ -834,7 +834,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':");
|
||||
"{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'c':'en','v':'v2'}]},'cd':");
|
||||
}
|
||||
|
||||
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
|
||||
@@ -1065,7 +1065,7 @@ namespace Umbraco.Tests.Services
|
||||
// both value11 and value21 are variant
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composed.Variations = ContentVariation.Nothing;
|
||||
ServiceContext.ContentTypeService.Save(composed);
|
||||
@@ -1073,7 +1073,7 @@ namespace Umbraco.Tests.Services
|
||||
// both value11 and value21 are invariant
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composed.Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composed);
|
||||
@@ -1081,7 +1081,7 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is variant again, but value21 is still invariant
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composed);
|
||||
@@ -1089,7 +1089,7 @@ namespace Umbraco.Tests.Services
|
||||
// we can make it variant again
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composing.Variations = ContentVariation.Nothing;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1097,7 +1097,7 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is invariant
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composing.Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1105,7 +1105,7 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is still invariant
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1113,7 +1113,7 @@ namespace Umbraco.Tests.Services
|
||||
// we can make it variant again
|
||||
Console.WriteLine(GetJson(document.Id));
|
||||
AssertJsonStartsWith(document.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -1178,11 +1178,11 @@ namespace Umbraco.Tests.Services
|
||||
// both value11 and value21 are variant
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composed1.Variations = ContentVariation.Nothing;
|
||||
ServiceContext.ContentTypeService.Save(composed1);
|
||||
@@ -1190,11 +1190,11 @@ namespace Umbraco.Tests.Services
|
||||
// both value11 and value21 are invariant
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composed1.Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composed1);
|
||||
@@ -1202,11 +1202,11 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is variant again, but value21 is still invariant
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composed1);
|
||||
@@ -1214,11 +1214,11 @@ namespace Umbraco.Tests.Services
|
||||
// we can make it variant again
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composing.Variations = ContentVariation.Nothing;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1226,11 +1226,11 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is invariant
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composing.Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1238,11 +1238,11 @@ namespace Umbraco.Tests.Services
|
||||
// value11 is still invariant
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
|
||||
composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture;
|
||||
ServiceContext.ContentTypeService.Save(composing);
|
||||
@@ -1250,11 +1250,11 @@ namespace Umbraco.Tests.Services
|
||||
// we can make it variant again
|
||||
Console.WriteLine(GetJson(document1.Id));
|
||||
AssertJsonStartsWith(document1.Id,
|
||||
"{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':");
|
||||
|
||||
Console.WriteLine(GetJson(document2.Id));
|
||||
AssertJsonStartsWith(document2.Id,
|
||||
"{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':");
|
||||
"{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':");
|
||||
}
|
||||
|
||||
private void CreateFrenchAndEnglishLangs()
|
||||
|
||||
@@ -313,6 +313,7 @@ namespace Umbraco.Tests.Testing
|
||||
Composition.RegisterUnique<ISectionService, SectionService>();
|
||||
|
||||
Composition.RegisterUnique<HtmlLocalLinkParser>();
|
||||
Composition.RegisterUnique<IEmailSender, EmailSender>();
|
||||
Composition.RegisterUnique<HtmlUrlParser>();
|
||||
Composition.RegisterUnique<HtmlImageSourceParser>();
|
||||
Composition.RegisterUnique<RichTextEditorPastedImages>();
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
<Compile Include="Configurations\GlobalSettingsTests.cs" />
|
||||
<Compile Include="CoreThings\CallContextTests.cs" />
|
||||
<Compile Include="Components\ComponentTests.cs" />
|
||||
<Compile Include="CoreThings\ClaimsIdentityExtensionsTests.cs" />
|
||||
<Compile Include="CoreThings\EnumExtensionsTests.cs" />
|
||||
<Compile Include="CoreThings\GuidUtilsTests.cs" />
|
||||
<Compile Include="CoreThings\HexEncoderTests.cs" />
|
||||
@@ -143,7 +144,13 @@
|
||||
<Compile Include="Persistence\Mappers\MapperTestBase.cs" />
|
||||
<Compile Include="Persistence\Repositories\DocumentRepositoryTest.cs" />
|
||||
<Compile Include="Persistence\Repositories\EntityRepositoryTest.cs" />
|
||||
<Compile Include="Security\AuthenticationExtensionsTests.cs" />
|
||||
<Compile Include="Security\BackOfficeClaimsPrincipalFactoryTests.cs" />
|
||||
<Compile Include="Persistence\Repositories\KeyValueRepositoryTests.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManagerTests.cs" />
|
||||
<Compile Include="Security\NopLookupNormalizerTests.cs" />
|
||||
<Compile Include="Security\OwinDataProtectorTokenProviderTests.cs" />
|
||||
<Compile Include="Security\UmbracoSecurityStampValidatorTests.cs" />
|
||||
<Compile Include="Services\KeyValueServiceTests.cs" />
|
||||
<Compile Include="Persistence\Repositories\UserRepositoryTest.cs" />
|
||||
<Compile Include="UmbracoExamine\ExamineExtensions.cs" />
|
||||
@@ -176,8 +183,6 @@
|
||||
<Compile Include="Published\ModelTypeTests.cs" />
|
||||
<Compile Include="Published\NestedContentTests.cs" />
|
||||
<Compile Include="Published\PropertyCacheLevelTests.cs" />
|
||||
<Compile Include="Misc\DateTimeExtensionsTests.cs" />
|
||||
<Compile Include="Misc\HashGeneratorTests.cs" />
|
||||
<Compile Include="IO\ShadowFileSystemTests.cs" />
|
||||
<Compile Include="Issues\U9560.cs" />
|
||||
<Compile Include="Integration\ContentEventsTests.cs" />
|
||||
@@ -366,7 +371,6 @@
|
||||
<Compile Include="PublishedContent\PublishedContentTestBase.cs" />
|
||||
<Compile Include="PublishedContent\PublishedContentTests.cs" />
|
||||
<Compile Include="PublishedContent\PublishedMediaTests.cs" />
|
||||
<Compile Include="Misc\HashCodeCombinerTests.cs" />
|
||||
<Compile Include="Web\Mvc\HtmlHelperExtensionMethodsTests.cs" />
|
||||
<Compile Include="IO\IoHelperTests.cs" />
|
||||
<Compile Include="PropertyEditors\PropertyEditorValueConverterTests.cs" />
|
||||
@@ -452,7 +456,6 @@
|
||||
<Compile Include="UmbracoExamine\ExamineDemoDataMediaService.cs" />
|
||||
<Compile Include="UmbracoExamine\ExamineDemoDataContentService.cs" />
|
||||
<Compile Include="CoreThings\UriExtensionsTests.cs" />
|
||||
<Compile Include="Misc\UriUtilityTests.cs" />
|
||||
<Compile Include="Composing\PackageActionCollectionTests.cs" />
|
||||
<Compile Include="Composing\TypeLoaderExtensions.cs" />
|
||||
<Compile Include="Composing\TypeLoaderTests.cs" />
|
||||
@@ -471,7 +474,6 @@
|
||||
<Compile Include="Web\UrlHelperExtensionTests.cs" />
|
||||
<Compile Include="Web\WebExtensionMethodTests.cs" />
|
||||
<Compile Include="CoreThings\XmlExtensionsTests.cs" />
|
||||
<Compile Include="Misc\XmlHelperTests.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\DictionaryPublishedContent.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\DomainCache.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\PreviewContent.cs" />
|
||||
|
||||
@@ -91,7 +91,8 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
Factory.GetInstance<IRuntimeState>(),
|
||||
Factory.GetInstance<UmbracoMapper>(),
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>()
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<IEmailSender>()
|
||||
);
|
||||
return usersController;
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
var display = JsonConvert.DeserializeObject<ContentItemDisplay>(response.Item2);
|
||||
Assert.AreEqual(2, display.Errors.Count());
|
||||
Assert.IsTrue(display.Errors.ContainsKey("Variants[0].Name"));
|
||||
Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_"));
|
||||
Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_null_"));
|
||||
}
|
||||
|
||||
// TODO: There are SOOOOO many more tests we should write - a lot of them to do with validation
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Http;
|
||||
using System.Web.Http.Controllers;
|
||||
using System.Web.Http.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
@@ -19,12 +26,10 @@ using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Persistence.Mappers;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.SqlSyntax;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Umbraco.Tests.TestHelpers.ControllerTesting;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
@@ -39,6 +44,9 @@ using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Core.Media;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Web.Controllers
|
||||
{
|
||||
@@ -62,7 +70,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task Save_User()
|
||||
public async Task Save_User()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -95,7 +103,8 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>()
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IEmailSender>()
|
||||
|
||||
);
|
||||
return usersController;
|
||||
@@ -149,7 +158,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_Empty()
|
||||
public async Task GetPagedUsers_Empty()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -168,7 +177,8 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>()
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IEmailSender>()
|
||||
);
|
||||
return usersController;
|
||||
}
|
||||
@@ -183,7 +193,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_10()
|
||||
public async Task GetPagedUsers_10()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -211,7 +221,8 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>()
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IEmailSender>()
|
||||
);
|
||||
return usersController;
|
||||
}
|
||||
@@ -227,7 +238,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_Fips()
|
||||
public async Task GetPagedUsers_Fips()
|
||||
{
|
||||
await RunFipsTest("GetPagedUsers", mock =>
|
||||
{
|
||||
@@ -246,7 +257,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetById_Fips()
|
||||
public async Task GetById_Fips()
|
||||
{
|
||||
const int mockUserId = 1234;
|
||||
var user = MockedUser.CreateUser();
|
||||
@@ -264,7 +275,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
|
||||
private async System.Threading.Tasks.Task RunFipsTest(string action, Action<Mock<IUserService>> userServiceSetup,
|
||||
private async Task RunFipsTest(string action, Action<Mock<IUserService>> userServiceSetup,
|
||||
Action<Tuple<HttpResponseMessage, string>> verification,
|
||||
object routeDefaults = null, string url = null)
|
||||
{
|
||||
@@ -289,7 +300,8 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>()
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IEmailSender>()
|
||||
);
|
||||
return usersController;
|
||||
}
|
||||
@@ -324,5 +336,186 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_UserIds_Not_Supplied_Expect_Ok_Response()
|
||||
{
|
||||
var usersController = CreateSut();
|
||||
|
||||
usersController.Request = new HttpRequestMessage();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new int[0]);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PostUnlockUsers_When_User_Does_Not_Exist_Expect_InvalidOperationException()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((BackOfficeIdentityUser) null);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await usersController.PostUnlockUsers(new[] {1}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_User_Lockout_Update_Fails_Expect_Failure_Response()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
const string expectedMessage = "identity error!";
|
||||
var user = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(user);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny<DateTimeOffset?>()))
|
||||
.ReturnsAsync(IdentityResult.Failed(new IdentityError {Description = expectedMessage}));
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new[] { 1 });
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.True(response.Headers.TryGetValues("X-Status-Reason", out var values));
|
||||
Assert.True(values.Contains("Validation failed"));
|
||||
|
||||
var responseContent = response.Content as ObjectContent<HttpError>;
|
||||
var responseValue = responseContent?.Value as HttpError;
|
||||
Assert.NotNull(responseValue);
|
||||
Assert.True(responseValue.Message.Contains(expectedMessage));
|
||||
Assert.True(responseValue.Message.Contains(user.Id.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_One_UserId_Supplied_Expect_User_Locked_Out_With_Correct_Response_Message()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
var user = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user.Id.ToString()))
|
||||
.ReturnsAsync(user);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new[] { user.Id });
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var responseContent = response.Content as ObjectContent<SimpleNotificationModel>;
|
||||
var notifications = responseContent?.Value as SimpleNotificationModel;
|
||||
Assert.NotNull(notifications);
|
||||
Assert.AreEqual(user.Name, notifications.Message);
|
||||
mockUserManager.Verify();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_Multiple_UserIds_Supplied_Expect_User_Locked_Out_With_Correct_Response_Message()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
var user1 = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
var user2 = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
2,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "alice"
|
||||
};
|
||||
var userIdsToLock = new[] {user1.Id, user2.Id};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user1.Id.ToString()))
|
||||
.ReturnsAsync(user1);
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user2.Id.ToString()))
|
||||
.ReturnsAsync(user2);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user1, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user2, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(userIdsToLock);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var responseContent = response.Content as ObjectContent<SimpleNotificationModel>;
|
||||
var notifications = responseContent?.Value as SimpleNotificationModel;
|
||||
Assert.NotNull(notifications);
|
||||
Assert.AreEqual(userIdsToLock.Length.ToString(), notifications.Message);
|
||||
mockUserManager.Verify();
|
||||
}
|
||||
|
||||
private UsersController CreateSut(IMock<BackOfficeUserManager<BackOfficeIdentityUser>> mockUserManager = null)
|
||||
{
|
||||
var mockLocalizedTextService = new Mock<ILocalizedTextService>();
|
||||
mockLocalizedTextService.Setup(x => x.Localize(It.IsAny<string>(), It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>()))
|
||||
.Returns((string key, CultureInfo ci, IDictionary<string, string> tokens)
|
||||
=> tokens.Aggregate("", (current, next) => current + (current == string.Empty ? "" : ",") + next.Value));
|
||||
|
||||
var usersController = new UsersController(
|
||||
Factory.GetInstance<IGlobalSettings>(),
|
||||
Factory.GetInstance<IUmbracoContextAccessor>(),
|
||||
Factory.GetInstance<ISqlContext>(),
|
||||
ServiceContext.CreatePartial(localizedTextService: mockLocalizedTextService.Object),
|
||||
Factory.GetInstance<AppCaches>(),
|
||||
Factory.GetInstance<IProfilingLogger>(),
|
||||
Factory.GetInstance<IRuntimeState>(),
|
||||
Factory.GetInstance<IMediaFileSystem>(),
|
||||
ShortStringHelper,
|
||||
Factory.GetInstance<UmbracoMapper>(),
|
||||
Factory.GetInstance<IContentSettings>(),
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>(),
|
||||
Factory.GetInstance<IEmailSender>());
|
||||
|
||||
var mockOwinContext = new Mock<IOwinContext>();
|
||||
var mockUserManagerMarker = new Mock<IBackOfficeUserManagerMarker>();
|
||||
|
||||
mockOwinContext.Setup(x => x.Get<IBackOfficeUserManagerMarker>(It.IsAny<string>()))
|
||||
.Returns(mockUserManagerMarker.Object);
|
||||
mockUserManagerMarker.Setup(x => x.GetManager(It.IsAny<IOwinContext>()))
|
||||
.Returns(mockUserManager?.Object ?? CreateMockUserManager().Object);
|
||||
|
||||
usersController.Request = new HttpRequestMessage();
|
||||
usersController.Request.Properties["MS_OwinContext"] = mockOwinContext.Object;
|
||||
usersController.Request.Properties[HttpPropertyKeys.RequestContextKey] = new HttpRequestContext {Configuration = new HttpConfiguration()};
|
||||
|
||||
return usersController;
|
||||
}
|
||||
|
||||
private static Mock<BackOfficeUserManager<BackOfficeIdentityUser>> CreateMockUserManager()
|
||||
{
|
||||
return new Mock<BackOfficeUserManager<BackOfficeIdentityUser>>(
|
||||
new Mock<IPasswordConfiguration>().Object,
|
||||
new Mock<IIpResolver>().Object,
|
||||
new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,19 @@ namespace Umbraco.Tests.Web
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property
|
||||
ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property
|
||||
|
||||
var result = ms.GetCulturesWithErrors(localizationService.Object, "en-US");
|
||||
var result = ms.GetVariantsWithErrors("en-US");
|
||||
|
||||
//even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
|
||||
ms = new ModelStateDictionary();
|
||||
ms.AddCultureValidationError("en-US", "generic culture error");
|
||||
ms.AddVariantValidationError("en-US", null, "generic culture error");
|
||||
|
||||
result = ms.GetCulturesWithErrors(localizationService.Object, "en-US");
|
||||
result = ms.GetVariantsWithErrors("en-US");
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -47,11 +47,11 @@ namespace Umbraco.Tests.Web
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property
|
||||
ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property
|
||||
|
||||
var result = ms.GetCulturesWithPropertyErrors(localizationService.Object, "en-US");
|
||||
var result = ms.GetVariantsWithPropertyErrors("en-US");
|
||||
|
||||
//even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -63,7 +63,7 @@ namespace Umbraco.Tests.Web
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.invariant", ms.Keys.First());
|
||||
Assert.AreEqual("_Properties.headerImage.invariant.null", ms.Keys.First());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -73,9 +73,57 @@ namespace Umbraco.Tests.Web
|
||||
var localizationService = new Mock<ILocalizationService>();
|
||||
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US");
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //invariant property
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //variant property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.en-US", ms.Keys.First());
|
||||
Assert.AreEqual("_Properties.headerImage.en-US.null", ms.Keys.First());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_Invariant_Segment_Property_Error()
|
||||
{
|
||||
var ms = new ModelStateDictionary();
|
||||
var localizationService = new Mock<ILocalizationService>();
|
||||
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US");
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null, "mySegment"); //invariant/segment property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.invariant.mySegment", ms.Keys.First());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_Variant_Segment_Property_Error()
|
||||
{
|
||||
var ms = new ModelStateDictionary();
|
||||
var localizationService = new Mock<ILocalizationService>();
|
||||
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US");
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US", "mySegment"); //variant/segment property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.en-US.mySegment", ms.Keys.First());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_Invariant_Segment_Field_Property_Error()
|
||||
{
|
||||
var ms = new ModelStateDictionary();
|
||||
var localizationService = new Mock<ILocalizationService>();
|
||||
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US");
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", null, "mySegment"); //invariant/segment property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.invariant.mySegment.myField", ms.Keys.First());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_Variant_Segment_Field_Property_Error()
|
||||
{
|
||||
var ms = new ModelStateDictionary();
|
||||
var localizationService = new Mock<ILocalizationService>();
|
||||
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US");
|
||||
|
||||
ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", "en-US", "mySegment"); //variant/segment property
|
||||
|
||||
Assert.AreEqual("_Properties.headerImage.en-US.mySegment.myField", ms.Keys.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends a custom response header to notify the UI that the current user data has been modified
|
||||
/// </summary>
|
||||
public sealed class AppendUserModifiedHeaderAttribute : ActionFilterAttribute
|
||||
{
|
||||
private readonly string _userIdParameter;
|
||||
|
||||
/// <summary>
|
||||
/// An empty constructor which will always set the header.
|
||||
/// </summary>
|
||||
public AppendUserModifiedHeaderAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A constructor specifying the action parameter name containing the user id to match against the
|
||||
/// current user and if they match the header will be appended.
|
||||
/// </summary>
|
||||
/// <param name="userIdParameter"></param>
|
||||
public AppendUserModifiedHeaderAttribute(string userIdParameter)
|
||||
{
|
||||
_userIdParameter = userIdParameter ?? throw new ArgumentNullException(nameof(userIdParameter));
|
||||
}
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (_userIdParameter.IsNullOrWhiteSpace())
|
||||
{
|
||||
AppendHeader(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!context.ActionArguments.ContainsKey(_userIdParameter))
|
||||
{
|
||||
throw new InvalidOperationException($"No argument found for the current action with the name: {_userIdParameter}");
|
||||
}
|
||||
|
||||
var umbracoContextAccessor = context.HttpContext.RequestServices.GetService<IUmbracoContextAccessor>();
|
||||
var user = umbracoContextAccessor.UmbracoContext.Security.CurrentUser;
|
||||
if (user == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = GetUserIdFromParameter(context.ActionArguments[_userIdParameter]);
|
||||
if (userId == user.Id)
|
||||
{
|
||||
AppendHeader(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AppendHeader(ActionExecutingContext context)
|
||||
{
|
||||
const string HeaderName = "X-Umb-User-Modified";
|
||||
if (context.HttpContext.Response.Headers.ContainsKey(HeaderName) == false)
|
||||
{
|
||||
context.HttpContext.Response.Headers.Add(HeaderName, "1");
|
||||
}
|
||||
}
|
||||
|
||||
private int GetUserIdFromParameter(object parameterValue)
|
||||
{
|
||||
if (parameterValue is int)
|
||||
{
|
||||
return (int)parameterValue;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"The id type: {parameterValue.GetType()} is not a supported id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Web.Common.Extensions;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
public class OnlyLocalRequestsAttribute : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (!context.HttpContext.Request.IsLocal())
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// An action filter used to do basic validation against the model and return a result
|
||||
/// straight away if it fails.
|
||||
/// </summary>
|
||||
internal sealed class ValidationFilterAttribute : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var modelState = context.ModelState;
|
||||
if (!modelState.IsValid)
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(modelState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Configuration\Umbraco.Configuration.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Umbraco.Web.Common.Extensions
|
||||
{
|
||||
public static class HttpRequestExtensions
|
||||
{
|
||||
internal static string ClientCulture(this HttpRequest request)
|
||||
{
|
||||
return request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a request is local.
|
||||
/// </summary>
|
||||
/// <returns>True if request is local</returns>
|
||||
/// <remarks>
|
||||
/// Hat-tip: https://stackoverflow.com/a/41242493/489433
|
||||
/// </remarks>
|
||||
public static bool IsLocal(this HttpRequest request)
|
||||
{
|
||||
var connection = request.HttpContext.Connection;
|
||||
if (connection.RemoteIpAddress.IsSet())
|
||||
{
|
||||
// We have a remote address set up
|
||||
return connection.LocalIpAddress.IsSet()
|
||||
// Is local is same as remote, then we are local
|
||||
? connection.RemoteIpAddress.Equals(connection.LocalIpAddress)
|
||||
// else we are remote if the remote IP address is not a loopback address
|
||||
: IPAddress.IsLoopback(connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsSet(this IPAddress address)
|
||||
{
|
||||
const string NullIpAddress = "::1";
|
||||
return address != null && address.ToString() != NullIpAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ namespace Umbraco.Web.Common.Extensions
|
||||
var connStrings = configs.ConnectionStrings();
|
||||
var appSettingMainDomLock = globalSettings.MainDomLock;
|
||||
var mainDomLock = appSettingMainDomLock == "SqlMainDomLock"
|
||||
? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator)
|
||||
? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator, hostingEnvironment)
|
||||
: new MainDomSemaphoreLock(logger, hostingEnvironment);
|
||||
|
||||
var mainDom = new MainDom(logger, mainDomLock);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Common.ModelBinders;
|
||||
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception filter checking if we get a <see cref="ModelBindingException" /> or <see cref="InvalidCastException" /> with the same model.
|
||||
/// In which case it returns a redirect to the same page after 1 sec if not in debug mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only enabled when running PureLive
|
||||
/// </remarks>
|
||||
internal class ModelBindingExceptionFilter : ActionFilterAttribute, IExceptionFilter
|
||||
{
|
||||
private static readonly Regex _getPublishedModelsTypesRegex = new Regex("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled);
|
||||
|
||||
private readonly IExceptionFilterSettings _exceptionFilterSettings;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
|
||||
public ModelBindingExceptionFilter(IExceptionFilterSettings exceptionFilterSettings, IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_exceptionFilterSettings = exceptionFilterSettings;
|
||||
_publishedModelFactory = publishedModelFactory ?? throw new ArgumentNullException(nameof(publishedModelFactory));
|
||||
}
|
||||
|
||||
public void OnException(ExceptionContext filterContext)
|
||||
{
|
||||
var disabled = _exceptionFilterSettings?.Disabled ?? false;
|
||||
if (_publishedModelFactory.IsLiveFactory()
|
||||
&& !disabled
|
||||
&& !filterContext.ExceptionHandled
|
||||
&& ((filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException)
|
||||
&& IsMessageAboutTheSameModelType(filterContext.Exception.Message)))
|
||||
{
|
||||
filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1");
|
||||
filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.GetEncodedUrl(), false);
|
||||
|
||||
filterContext.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the message is about two models with the same name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Message could be something like:
|
||||
/// <para>
|
||||
/// InvalidCastException:
|
||||
/// [A]Umbraco.Web.PublishedModels.Home cannot be cast to [B]Umbraco.Web.PublishedModels.Home. Type A originates from 'App_Web_all.generated.cs.8f9494c4.rtdigm_z, Version=0.0.0.3, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rtdigm_z.dll'. Type B originates from 'App_Web_all.generated.cs.8f9494c4.rbyqlplu, Version=0.0.0.5, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rbyqlplu.dll'.
|
||||
///</para>
|
||||
/// <para>
|
||||
/// ModelBindingException:
|
||||
/// Cannot bind source content type Umbraco.Web.PublishedModels.Home to model type Umbraco.Web.PublishedModels.Home. Both view and content models are PureLive, with different versions. The application is in an unstable state and is going to be restarted. The application is restarting now.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private bool IsMessageAboutTheSameModelType(string exceptionMessage)
|
||||
{
|
||||
var matches = _getPublishedModelsTypesRegex.Matches(exceptionMessage);
|
||||
|
||||
if (matches.Count >= 2)
|
||||
{
|
||||
return string.Equals(matches[0].Value, matches[1].Value, StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Models;
|
||||
|
||||
namespace Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps view models, supporting mapping to and from any IPublishedContent or IContentModel.
|
||||
/// </summary>
|
||||
public class ContentModelBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext.ActionContext.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out var source) == false)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// This model binder deals with IContentModel and IPublishedContent by extracting the model from the route's
|
||||
// datatokens. This data token is set in 2 places: RenderRouteHandler, UmbracoVirtualNodeRouteHandler
|
||||
// and both always set the model to an instance of `ContentModel`.
|
||||
|
||||
// No need for type checks to ensure we have the appropriate binder, as in .NET Core this is handled in the provider,
|
||||
// in this case ContentModelBinderProvider.
|
||||
|
||||
// Being defensice though.... if for any reason the model is not either IContentModel or IPublishedContent,
|
||||
// then we return since those are the only types this binder is dealing with.
|
||||
if (source is IContentModel == false && source is IPublishedContent == false)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
BindModelAsync(bindingContext, source, bindingContext.ModelType);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// source is the model that we have
|
||||
// modelType is the type of the model that we need to bind to
|
||||
//
|
||||
// create a model object of the modelType by mapping:
|
||||
// { ContentModel, ContentModel<TContent>, IPublishedContent }
|
||||
// to
|
||||
// { ContentModel, ContentModel<TContent>, IPublishedContent }
|
||||
//
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext, object source, Type modelType)
|
||||
{
|
||||
// Null model, return
|
||||
if (source == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// If types already match, return
|
||||
var sourceType = source.GetType();
|
||||
if (sourceType.Inherits(modelType)) // includes ==
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Try to grab the content
|
||||
var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent
|
||||
if (sourceContent == null && sourceType.Implements<IContentModel>())
|
||||
// else check if it's an IContentModel, and get the content
|
||||
sourceContent = ((IContentModel)source).Content;
|
||||
if (sourceContent == null)
|
||||
{
|
||||
// else check if we can convert it to a content
|
||||
var attempt1 = source.TryConvertTo<IPublishedContent>();
|
||||
if (attempt1.Success) sourceContent = attempt1.Result;
|
||||
}
|
||||
|
||||
// If we have a content
|
||||
if (sourceContent != null)
|
||||
{
|
||||
// If model is IPublishedContent, check content type and return
|
||||
if (modelType.Implements<IPublishedContent>())
|
||||
{
|
||||
if (sourceContent.GetType().Inherits(modelType) == false)
|
||||
{
|
||||
ThrowModelBindingException(true, false, sourceContent.GetType(), modelType);
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(sourceContent);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// If model is ContentModel, create and return
|
||||
if (modelType == typeof(ContentModel))
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// If model is ContentModel<TContent>, check content type, then create and return
|
||||
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>))
|
||||
{
|
||||
var targetContentType = modelType.GetGenericArguments()[0];
|
||||
if (sourceContent.GetType().Inherits(targetContentType) == false)
|
||||
{
|
||||
ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType);
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// Last chance : try to convert
|
||||
var attempt2 = source.TryConvertTo(modelType);
|
||||
if (attempt2.Success)
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(attempt2.Result);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Fail
|
||||
ThrowModelBindingException(false, false, sourceType, modelType);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType)
|
||||
{
|
||||
var msg = new StringBuilder();
|
||||
|
||||
// prepare message
|
||||
msg.Append("Cannot bind source");
|
||||
if (sourceContent) msg.Append(" content");
|
||||
msg.Append(" type ");
|
||||
msg.Append(sourceType.FullName);
|
||||
msg.Append(" to model");
|
||||
if (modelContent) msg.Append(" content");
|
||||
msg.Append(" type ");
|
||||
msg.Append(modelType.FullName);
|
||||
msg.Append(".");
|
||||
|
||||
// raise event, to give model factories a chance at reporting
|
||||
// the error with more details, and optionally request that
|
||||
// the application restarts.
|
||||
|
||||
var args = new ModelBindingArgs(sourceType, modelType, msg);
|
||||
ModelBindingException?.Invoke(this, args);
|
||||
|
||||
throw new ModelBindingException(msg.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains event data for the <see cref="ModelBindingException"/> event.
|
||||
/// </summary>
|
||||
public class ModelBindingArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModelBindingArgs"/> class.
|
||||
/// </summary>
|
||||
public ModelBindingArgs(Type sourceType, Type modelType, StringBuilder message)
|
||||
{
|
||||
SourceType = sourceType;
|
||||
ModelType = modelType;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the source object.
|
||||
/// </summary>
|
||||
public Type SourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the view model.
|
||||
/// </summary>
|
||||
public Type ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message string builder.
|
||||
/// </summary>
|
||||
/// <remarks>Handlers of the event can append text to the message.</remarks>
|
||||
public StringBuilder Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the application should restart.
|
||||
/// </summary>
|
||||
public bool Restart { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs on model binding exceptions.
|
||||
/// </summary>
|
||||
public static event EventHandler<ModelBindingArgs> ModelBindingException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Models;
|
||||
|
||||
namespace Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// The provider for <see cref="ContentModelBinder"/> mapping view models, supporting mapping to and from any IPublishedContent or IContentModel.
|
||||
/// </summary>
|
||||
public class ContentModelBinderProvider : IModelBinderProvider
|
||||
{
|
||||
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||
{
|
||||
var modelType = context.Metadata.ModelType;
|
||||
|
||||
// Can bind to ContentModel (exact type match)
|
||||
// or to ContentModel<TContent> (exact generic type match)
|
||||
// or to TContent where TContent : IPublishedContent (any IPublishedContent implementation)
|
||||
if (modelType == typeof(ContentModel) ||
|
||||
(modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) ||
|
||||
typeof(IPublishedContent).IsAssignableFrom(modelType))
|
||||
{
|
||||
return new BinderTypeModelBinder(typeof(ContentModelBinder));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Common.Extensions;
|
||||
|
||||
namespace Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows an Action to execute with an arbitrary number of QueryStrings
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number
|
||||
/// but this will allow you to do it.
|
||||
/// </remarks>
|
||||
public sealed class HttpQueryStringModelBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
var queryStrings = GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query);
|
||||
var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray();
|
||||
if (queryStringKeys.InvariantContains("culture") == false)
|
||||
{
|
||||
queryStrings.Add("culture", new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture()));
|
||||
}
|
||||
|
||||
var formData = new FormCollection(queryStrings);
|
||||
bindingContext.Result = ModelBindingResult.Success(formData);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Dictionary<string, StringValues> GetQueryAsDictionary(IQueryCollection query)
|
||||
{
|
||||
var result = new Dictionary<string, StringValues>();
|
||||
if (query == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var item in query)
|
||||
{
|
||||
result.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Web.Common.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when an error occurs while binding a source to a model.
|
||||
/// </summary>
|
||||
/// <seealso cref="Exception" />
|
||||
/// Migrated to .NET Core
|
||||
[Serializable]
|
||||
public class ModelBindingException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
|
||||
/// </summary>
|
||||
public ModelBindingException()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public ModelBindingException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference (<see langword="Nothing" /> in Visual Basic) if no inner exception is specified.</param>
|
||||
public ModelBindingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
|
||||
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual information about the source or destination.</param>
|
||||
protected ModelBindingException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,16 @@
|
||||
module.exports = {
|
||||
compile: {
|
||||
build: {
|
||||
sourcemaps: false
|
||||
sourcemaps: false,
|
||||
embedtemplates: true
|
||||
},
|
||||
dev: {
|
||||
sourcemaps: true
|
||||
sourcemaps: true,
|
||||
embedtemplates: true
|
||||
},
|
||||
test: {
|
||||
sourcemaps: false,
|
||||
embedtemplates: true
|
||||
}
|
||||
},
|
||||
sources: {
|
||||
@@ -17,7 +23,7 @@ module.exports = {
|
||||
installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" },
|
||||
nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"},
|
||||
preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" },
|
||||
umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" },
|
||||
umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" },
|
||||
rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" }
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user