Merge branch 'v14/dev' into v15/dev
# Conflicts: # src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs # src/Umbraco.Web.Common/Views/UmbracoViewPage.cs
This commit is contained in:
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
|
||||
// a redirect with "{root content ID}/{content path}"
|
||||
if (redirectUrl is null && startItem is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
|
||||
}
|
||||
|
||||
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
|
||||
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
|
||||
if (redirectUrl is null)
|
||||
{
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
}
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions(Func<int> performCount)
|
||||
{
|
||||
PerformCount = performCount;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = true;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions()
|
||||
{
|
||||
PerformCount = null;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = false;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
|
||||
/// </summary>
|
||||
public Func<int>? PerformCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
|
||||
/// </summary>
|
||||
public bool CacheNullValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
|
||||
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
|
||||
|
||||
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
|
||||
internal const string StaticModelsDirectory = "~/umbraco/models";
|
||||
internal const bool StaticAcceptUnsafeModelsDirectory = false;
|
||||
internal const int StaticDebugLevel = 0;
|
||||
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
|
||||
private bool _flagOutOfDateModels = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
|
||||
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
|
||||
[DefaultValue(StaticDebugLevel)]
|
||||
public int DebugLevel { get; set; } = StaticDebugLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the version number should be included in generated models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
|
||||
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
|
||||
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
|
||||
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
|
||||
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ public class SecuritySettings
|
||||
internal const bool StaticAllowEditInvariantFromNonDefault = false;
|
||||
internal const bool StaticAllowConcurrentLogins = false;
|
||||
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
|
||||
internal const bool StaticUsernameIsEmail = true;
|
||||
internal const bool StaticMemberRequireUniqueEmail = true;
|
||||
|
||||
internal const string StaticAllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
|
||||
@@ -64,7 +66,14 @@ public class SecuritySettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
|
||||
/// </summary>
|
||||
public bool UsernameIsEmail { get; set; } = true;
|
||||
[DefaultValue(StaticUsernameIsEmail)]
|
||||
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the member's email address must be unique.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMemberRequireUniqueEmail)]
|
||||
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of allowed characters for a username
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@using Umbraco.Extensions
|
||||
|
||||
@{
|
||||
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
|
||||
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
|
||||
var logoutModel = new PostRedirectModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
logoutModel.RedirectUrl = null;
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
<div class="login-status">
|
||||
|
||||
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
|
||||
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
|
||||
{
|
||||
|
||||
28
src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs
Normal file
28
src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies options for publishing notifcations when saving.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishNotificationSaveOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not publish any notifications.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saving notification.
|
||||
/// </summary>
|
||||
Saving = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saved notification.
|
||||
/// </summary>
|
||||
Saved = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Publish all the notifications.
|
||||
/// </summary>
|
||||
All = Saving | Saved,
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public interface ITagQuery
|
||||
/// <summary>
|
||||
/// Gets all document tags.
|
||||
/// </summary>
|
||||
/// /// <remarks>
|
||||
/// <remarks>
|
||||
/// If no culture is specified, it retrieves tags with an invariant culture.
|
||||
/// If a culture is specified, it only retrieves tags for that culture.
|
||||
/// Use "*" to retrieve tags for all cultures.
|
||||
|
||||
@@ -217,6 +217,15 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IMembershipUser" />
|
||||
/// </summary>
|
||||
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
|
||||
/// <param name="member"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
|
||||
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
|
||||
/// <param name="userId">Id of the User saving the Member</param>
|
||||
Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a single <see cref="IMember" /> object
|
||||
/// </summary>
|
||||
@@ -268,6 +277,21 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember? GetById(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Get an list of <see cref="IMember"/> for all members with the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns>
|
||||
/// <see cref="IEnumerable{IMember}" />
|
||||
/// </returns>
|
||||
IEnumerable<IMember> GetMembersByEmail(string email)
|
||||
=>
|
||||
// TODO (V16): Remove this default implementation.
|
||||
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
|
||||
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
|
||||
// does not override this method.
|
||||
GetAllMembers().Where(x => x.Email.Equals(email));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all Members for the specified MemberType alias
|
||||
/// </summary>
|
||||
|
||||
@@ -408,16 +408,23 @@ namespace Umbraco.Cms.Core.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an <see cref="IMember"/> by email
|
||||
/// Get an <see cref="IMember"/> by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns><see cref="IMember"/></returns>
|
||||
public IMember? GetByEmail(string email)
|
||||
public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Get an list of <see cref="IMember"/> for all members with the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns><see cref="IEnumerable{IMember}"/></returns>
|
||||
public IEnumerable<IMember> GetMembersByEmail(string email)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
|
||||
return _memberRepository.Get(query)?.FirstOrDefault();
|
||||
return _memberRepository.Get(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -765,6 +772,9 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<OperationResult?> Save(IMember member, int userId = Constants.Security.SuperUserId)
|
||||
=> Save(member, PublishNotificationSaveOptions.All, userId);
|
||||
|
||||
public Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
// trimming username and email to make sure we have no trailing space
|
||||
member.Username = member.Username.Trim();
|
||||
@@ -773,11 +783,15 @@ namespace Umbraco.Cms.Core.Services
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
var savingNotification = new MemberSavingNotification(member, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
MemberSavingNotification? savingNotification = null;
|
||||
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Attempt.Cancel(evtMsgs);
|
||||
savingNotification = new MemberSavingNotification(member, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Attempt.Cancel(evtMsgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(member.Name))
|
||||
@@ -789,7 +803,13 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
_memberRepository.Save(member);
|
||||
|
||||
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
|
||||
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
|
||||
{
|
||||
scope.Notifications.Publish(
|
||||
savingNotification is null
|
||||
? new MemberSavedNotification(member, evtMsgs)
|
||||
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
|
||||
}
|
||||
|
||||
Audit(AuditType.Save, userId, member.Id);
|
||||
|
||||
|
||||
@@ -87,6 +87,6 @@ public static class UserServiceExtensions
|
||||
});
|
||||
}
|
||||
|
||||
[Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")]
|
||||
[Obsolete("Use IUserService.GetAsync that takes a Guid instead. Scheduled for removal in V15.")]
|
||||
public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
|
||||
private readonly RepositoryCachePolicyOptions _options;
|
||||
|
||||
private const string NullRepresentationInCache = "*NULL*";
|
||||
|
||||
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
|
||||
: base(cache, scopeAccessor) =>
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
@@ -116,6 +118,7 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
{
|
||||
// whatever happens, clear the cache
|
||||
var cacheKey = GetEntityCacheKey(entity.Id);
|
||||
|
||||
Cache.Clear(cacheKey);
|
||||
|
||||
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
|
||||
@@ -127,20 +130,36 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
|
||||
{
|
||||
var cacheKey = GetEntityCacheKey(id);
|
||||
|
||||
TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
|
||||
|
||||
// if found in cache then return else fetch and cache
|
||||
if (fromCache != null)
|
||||
// If found in cache then return immediately.
|
||||
if (fromCache is not null)
|
||||
{
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
// Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
|
||||
// Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
|
||||
// If we've cached a "null" value, return null.
|
||||
if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == NullRepresentationInCache)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise go to the database to retrieve.
|
||||
TEntity? entity = performGet(id);
|
||||
|
||||
if (entity != null && entity.HasIdentity)
|
||||
{
|
||||
// If we've found an identified entity, cache it for subsequent retrieval.
|
||||
InsertEntity(cacheKey, entity);
|
||||
}
|
||||
else if (entity is null && _options.CacheNullValues)
|
||||
{
|
||||
// If we've not found an entity, and we're caching null values, cache a "null" value.
|
||||
InsertNull(cacheKey);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
@@ -248,6 +267,15 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
protected virtual void InsertEntity(string cacheKey, TEntity entity)
|
||||
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
|
||||
|
||||
protected virtual void InsertNull(string cacheKey)
|
||||
{
|
||||
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
|
||||
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
|
||||
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
|
||||
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
|
||||
Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
|
||||
}
|
||||
|
||||
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
|
||||
{
|
||||
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
|
||||
@@ -153,16 +153,26 @@ SELECT obj_Constraint.NAME AS 'constraintName'
|
||||
");
|
||||
var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
|
||||
|
||||
|
||||
// only rename the constraint if necessary
|
||||
// Only rename the constraint if necessary.
|
||||
if (currentConstraintName == expectedConstraintName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
|
||||
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
|
||||
Database.Execute(renameConstraintQuery);
|
||||
if (currentConstraintName is null)
|
||||
{
|
||||
// Constraint does not exist, so we need to create it.
|
||||
Sql<ISqlContext> createConstraintStatement = Database.SqlContext.Sql(@$"
|
||||
ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
|
||||
Database.Execute(createConstraintStatement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Constraint exists, and differs from the expected name, so we need to rename it.
|
||||
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
|
||||
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
|
||||
Database.Execute(renameConstraintQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
|
||||
|
||||
@@ -143,14 +143,17 @@ public class TextBuilder : Builder
|
||||
//
|
||||
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
|
||||
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
|
||||
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
|
||||
private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
|
||||
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
|
||||
tabs, ApiVersion.Current.Version);
|
||||
tabs,
|
||||
Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
|
||||
|
||||
// writes an attribute that specifies that an output may be null.
|
||||
// (useful for consuming projects with nullable reference types enabled)
|
||||
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
|
||||
sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
|
||||
sb.AppendFormat(
|
||||
"{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
|
||||
tabs,
|
||||
isReturn ? "return: " : string.Empty);
|
||||
|
||||
private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
|
||||
|
||||
@@ -122,11 +122,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
private IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary<int, ILanguage> languagesById)
|
||||
@@ -217,11 +216,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IDictionaryItem> PerformGetAll(params Guid[]? ids)
|
||||
@@ -272,12 +270,13 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
{
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow null to be cached
|
||||
CacheNullValues = true,
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IDictionaryItem> PerformGetAll(params string[]? ids)
|
||||
|
||||
@@ -389,7 +389,9 @@ WHERE r.tagId IS NULL";
|
||||
}).ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null,
|
||||
public IEnumerable<ITag> GetTagsForEntityType(
|
||||
TaggableObjectTypes objectType,
|
||||
string? group = null,
|
||||
string? culture = null)
|
||||
{
|
||||
Sql<ISqlContext> sql = GetTagsSql(culture, true);
|
||||
@@ -403,6 +405,9 @@ WHERE r.tagId IS NULL";
|
||||
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
|
||||
}
|
||||
|
||||
sql = sql
|
||||
.Where<NodeDto>(dto => !dto.Trashed);
|
||||
|
||||
if (group.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
sql = sql
|
||||
|
||||
@@ -5,6 +5,7 @@ using HtmlAgilityPack;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
@@ -33,6 +34,8 @@ public sealed class RichTextEditorPastedImages
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IMediaImportService _mediaImportService;
|
||||
private readonly IImageUrlGenerator _imageUrlGenerator;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
|
||||
@@ -84,6 +87,30 @@ public sealed class RichTextEditorPastedImages
|
||||
{
|
||||
}
|
||||
|
||||
// highest overload to be picked by DI, pointing to newest ctor
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ILogger<RichTextEditorPastedImages> logger,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IMediaService mediaService,
|
||||
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
|
||||
MediaFileManager mediaFileManager,
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IOptions<ContentSettings> contentSettings,
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches)
|
||||
: this(umbracoContextAccessor, publishedUrlProvider, temporaryFileService, scopeProvider, mediaImportService, imageUrlGenerator, entityService, appCaches)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
@@ -91,6 +118,27 @@ public sealed class RichTextEditorPastedImages
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator)
|
||||
: this(
|
||||
umbracoContextAccessor,
|
||||
publishedUrlProvider,
|
||||
temporaryFileService,
|
||||
scopeProvider,
|
||||
mediaImportService,
|
||||
imageUrlGenerator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
|
||||
{
|
||||
}
|
||||
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches)
|
||||
{
|
||||
_umbracoContextAccessor =
|
||||
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
@@ -99,6 +147,8 @@ public sealed class RichTextEditorPastedImages
|
||||
_scopeProvider = scopeProvider;
|
||||
_mediaImportService = mediaImportService;
|
||||
_imageUrlGenerator = imageUrlGenerator;
|
||||
_entityService = entityService;
|
||||
_appCaches = appCaches;
|
||||
|
||||
// this obviously is not correct. however, we only use IUserService in an obsolete method,
|
||||
// so this is better than having even more obsolete constructors for V16
|
||||
@@ -161,7 +211,7 @@ public sealed class RichTextEditorPastedImages
|
||||
if (uploadedImages.ContainsKey(temporaryFileKey) == false)
|
||||
{
|
||||
using Stream fileStream = temporaryFile.OpenReadStream();
|
||||
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? Constants.System.RootKey : mediaParentFolder;
|
||||
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? await GetDefaultMediaRoot(userKey) : mediaParentFolder;
|
||||
IMedia mediaFile = await _mediaImportService.ImportAsync(temporaryFile.FileName, fileStream, parentFolderKey, MediaTypeAlias(temporaryFile.FileName), userKey);
|
||||
udi = mediaFile.GetUdi();
|
||||
}
|
||||
@@ -214,6 +264,20 @@ public sealed class RichTextEditorPastedImages
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetDefaultMediaRoot(Guid userKey)
|
||||
{
|
||||
IUser user = await _userService.GetAsync(userKey) ?? throw new ArgumentException("User could not be found");
|
||||
var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
|
||||
var firstNodeId = userStartNodes?.FirstOrDefault();
|
||||
if (firstNodeId is null)
|
||||
{
|
||||
return Constants.System.RootKey;
|
||||
}
|
||||
|
||||
Attempt<Guid> firstNodeKeyAttempt = _entityService.GetKey(firstNodeId.Value, UmbracoObjectTypes.Media);
|
||||
return firstNodeKeyAttempt.Success ? firstNodeKeyAttempt.Result : Constants.System.RootKey;
|
||||
}
|
||||
|
||||
private string MediaTypeAlias(string fileName)
|
||||
=> fileName.InvariantEndsWith(".svg")
|
||||
? Constants.Conventions.MediaTypes.VectorGraphicsAlias
|
||||
|
||||
@@ -101,7 +101,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
|
||||
UpdateMemberProperties(memberEntity, user, out bool _);
|
||||
|
||||
// create the member
|
||||
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity);
|
||||
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity, PublishNotificationSaveOptions.Saving);
|
||||
if (saveAttempt.Success is false)
|
||||
{
|
||||
scope.Complete();
|
||||
|
||||
@@ -35,18 +35,29 @@ public abstract class UmbracoUserStore<TUser, TRole>
|
||||
[Obsolete("Use TryConvertIdentityIdToInt instead. Scheduled for removal in V15.")]
|
||||
protected static int UserIdToInt(string? userId)
|
||||
{
|
||||
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
if (TryUserIdToInt(userId, out int result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
|
||||
}
|
||||
|
||||
protected static bool TryUserIdToInt(string? userId, out int result)
|
||||
{
|
||||
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(userId, out Guid key))
|
||||
{
|
||||
// Reverse the IntExtensions.ToGuid
|
||||
return BitConverter.ToInt32(key.ToByteArray(), 0);
|
||||
result = BitConverter.ToInt32(key.ToByteArray(), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract Task<int> ResolveEntityIdFromIdentityId(string? identityId);
|
||||
|
||||
@@ -62,9 +62,16 @@ public static class HttpContextExtensions
|
||||
// Update the HttpContext's user with the authenticated user's principal to ensure
|
||||
// that subsequent requests within the same context will recognize the user
|
||||
// as authenticated.
|
||||
if (result.Succeeded)
|
||||
if (result is { Succeeded: true, Principal.Identity: not null })
|
||||
{
|
||||
httpContext.User = result.Principal;
|
||||
// We need to get existing identities that are not the backoffice kind and flow them to the new identity
|
||||
// Otherwise we can't log in as both a member and a backoffice user
|
||||
// For instance if you've enabled basic auth.
|
||||
ClaimsPrincipal? authenticatedPrincipal = result.Principal;
|
||||
IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
|
||||
authenticatedPrincipal.AddIdentities(existingIdentities);
|
||||
|
||||
httpContext.User = authenticatedPrincipal;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class MemberClaimsPrincipalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get specifically the member identity from the ClaimsPrincipal
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The identity returned is the one with default authentication type.
|
||||
/// </remarks>
|
||||
/// <param name="principal">The principal to find the identity in.</param>
|
||||
/// <returns>The default authenticated authentication type identity.</returns>
|
||||
public static ClaimsIdentity? GetMemberIdentity(this ClaimsPrincipal principal)
|
||||
=> principal.Identities.FirstOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public sealed class ConfigureMemberIdentityOptions : IConfigureOptions<IdentityO
|
||||
options.SignIn.RequireConfirmedEmail = false; // not implemented
|
||||
options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented
|
||||
|
||||
options.User.RequireUniqueEmail = true;
|
||||
options.User.RequireUniqueEmail = _securitySettings.MemberRequireUniqueEmail;
|
||||
|
||||
// Support validation of member names using Down-Level Logon Name format
|
||||
options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -113,8 +114,11 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
|
||||
/// <inheritdoc />
|
||||
public virtual bool IsLoggedIn()
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
return httpContext?.User.Identity?.IsAuthenticated ?? false;
|
||||
// We have to try and specifically find the member identity, it's entirely possible for there to be both backoffice and member.
|
||||
ClaimsIdentity? memberIdentity = _httpContextAccessor.HttpContext?.User.GetMemberIdentity();
|
||||
|
||||
return memberIdentity is not null &&
|
||||
memberIdentity.IsAuthenticated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -170,23 +174,27 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<MemberIdentityUser?> GetCurrentMemberAsync()
|
||||
{
|
||||
if (_currentMember == null)
|
||||
if (_currentMember is not null)
|
||||
{
|
||||
if (!IsLoggedIn())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User!);
|
||||
return _currentMember;
|
||||
}
|
||||
|
||||
if (IsLoggedIn() is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a principal the represents the member security context.
|
||||
var memberPrincipal = new ClaimsPrincipal(_httpContextAccessor.HttpContext?.User.GetMemberIdentity()!);
|
||||
_currentMember = await GetUserAsync(memberPrincipal);
|
||||
|
||||
return _currentMember;
|
||||
}
|
||||
|
||||
public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
|
||||
|
||||
/// <summary>
|
||||
/// This will check if the member has access to this path
|
||||
/// This will check if the member has access to this path.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -638,6 +638,89 @@ public class TagRepositoryTest : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Get_Tags_For_Entity_Type_Excluding_Trashed_Entity()
|
||||
{
|
||||
var provider = ScopeProvider;
|
||||
using (ScopeProvider.CreateScope())
|
||||
{
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
FileService.SaveTemplate(template);
|
||||
|
||||
var contentType = ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
|
||||
ContentTypeRepository.Save(contentType);
|
||||
|
||||
var content1 = ContentBuilder.CreateSimpleContent(contentType);
|
||||
content1.PublishCulture(CultureImpact.Invariant);
|
||||
content1.PublishedState = PublishedState.Publishing;
|
||||
DocumentRepository.Save(content1);
|
||||
|
||||
var content2 = ContentBuilder.CreateSimpleContent(contentType);
|
||||
content2.PublishCulture(CultureImpact.Invariant);
|
||||
content2.PublishedState = PublishedState.Publishing;
|
||||
content2.Trashed = true;
|
||||
DocumentRepository.Save(content2);
|
||||
|
||||
var mediaType = MediaTypeBuilder.CreateImageMediaType("image2");
|
||||
MediaTypeRepository.Save(mediaType);
|
||||
|
||||
var media1 = MediaBuilder.CreateMediaImage(mediaType, -1);
|
||||
MediaRepository.Save(media1);
|
||||
|
||||
var media2 = MediaBuilder.CreateMediaImage(mediaType, -1);
|
||||
media2.Trashed = true;
|
||||
MediaRepository.Save(media2);
|
||||
|
||||
var repository = CreateRepository(provider);
|
||||
Tag[] tags =
|
||||
{
|
||||
new Tag {Text = "tag1", Group = "test"},
|
||||
new Tag {Text = "tag2", Group = "test1"},
|
||||
new Tag {Text = "tag3", Group = "test"}
|
||||
};
|
||||
|
||||
Tag[] tags2 =
|
||||
{
|
||||
new Tag {Text = "tag4", Group = "test"},
|
||||
new Tag {Text = "tag5", Group = "test1"},
|
||||
new Tag {Text = "tag6", Group = "test"}
|
||||
};
|
||||
|
||||
repository.Assign(
|
||||
content1.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
content2.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags2,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
media1.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
media2.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags2,
|
||||
false);
|
||||
|
||||
var result1 = repository.GetTagsForEntityType(TaggableObjectTypes.Content).ToArray();
|
||||
var result2 = repository.GetTagsForEntityType(TaggableObjectTypes.Media).ToArray();
|
||||
var result3 = repository.GetTagsForEntityType(TaggableObjectTypes.All).ToArray();
|
||||
|
||||
const string ExpectedTags = "tag1,tag2,tag3";
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result1.Select(x => x.Text)));
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result2.Select(x => x.Text)));
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result3.Select(x => x.Text)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Get_Tags_For_Entity_Type()
|
||||
{
|
||||
|
||||
@@ -268,7 +268,7 @@ public class MemberManagerTests
|
||||
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(fakeMember);
|
||||
_mockMemberService
|
||||
.Setup(x => x.Save(fakeMember, Constants.Security.SuperUserId))
|
||||
.Setup(x => x.Save(fakeMember, It.IsAny<PublishNotificationSaveOptions>(), Constants.Security.SuperUserId))
|
||||
.Returns(Attempt.Succeed<OperationResult?>(null));
|
||||
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class MemberUserStoreTests
|
||||
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(mockMember);
|
||||
_mockMemberService
|
||||
.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId))
|
||||
.Setup(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId))
|
||||
.Returns(Attempt.Succeed<OperationResult?>(null));
|
||||
// act
|
||||
var identityResult = await sut.CreateAsync(fakeUser, CancellationToken.None);
|
||||
@@ -134,7 +134,7 @@ public class MemberUserStoreTests
|
||||
Assert.IsTrue(!identityResult.Errors.Any());
|
||||
_mockMemberService.Verify(x =>
|
||||
x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()));
|
||||
_mockMemberService.Verify(x => x.Save(mockMember, Constants.Security.SuperUserId));
|
||||
_mockMemberService.Verify(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user