From 38837049f019ad6d8fa9c59d2cee27c0a93e8f27 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 May 2017 19:01:01 +1000 Subject: [PATCH] adds invite user endpoint with a controller test! --- .../Configuration/GlobalSettings.cs | 24 +++++++- .../Controllers/UsersControllerTests.cs | 59 ++++++++++++++++++- .../TestHelpers/BaseUmbracoApplicationTest.cs | 32 +++++----- .../TestControllerActivatorBase.cs | 4 +- .../ControllerTesting/TestRunner.cs | 7 ++- .../Editors/BackOfficeController.cs | 14 +---- src/Umbraco.Web/Editors/UsersController.cs | 41 +++++++++++++ .../Models/ContentEditing/UserInvite.cs | 26 ++++++++ .../Models/ContentEditing/UserSave.cs | 4 +- .../Models/Mapping/UserModelMapper.cs | 10 ++++ .../Identity/ExternalSignInAutoLinkOptions.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 1 + 12 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/UserInvite.cs diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index acbf0065c0..02f3322ec9 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Configuration; using System.Linq; +using System.Net.Configuration; using System.Web; using System.Web.Configuration; using System.Web.Hosting; @@ -42,7 +43,6 @@ namespace Umbraco.Core.Configuration //ensure the built on (non-changeable) reserved paths are there at all times private const string StaticReservedPaths = "~/app_plugins/,~/install/,"; private const string StaticReservedUrls = "~/config/splashes/booting.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd,"; - #endregion /// @@ -53,6 +53,7 @@ namespace Umbraco.Core.Configuration _reservedUrlsCache = null; _reservedPaths = null; _reservedUrls = null; + HasSmtpServer = null; } /// @@ -64,7 +65,26 @@ namespace Umbraco.Core.Configuration ResetInternal(); } - /// + public static bool HasSmtpServerConfigured(string appPath) + { + if (HasSmtpServer.HasValue) return HasSmtpServer.Value; + + var config = WebConfigurationManager.OpenWebConfiguration(appPath); + var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); + if (settings == null || settings.Smtp == null) return false; + if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) + return true; + if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) + return true; + return false; + } + + /// + /// For testing only + /// + internal static bool? HasSmtpServer { get; set; } + + /// /// Gets the reserved urls from web.config. /// /// The reserved urls. diff --git a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs index 45a4979377..b722bc789b 100644 --- a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs @@ -32,14 +32,66 @@ using Umbraco.Web.WebApi; namespace Umbraco.Tests.Controllers { [DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)] - [RequiresAutoMapperMappings] [TestFixture] public class UsersControllerTests : BaseDatabaseFactoryTest { + + [Test] + public async void Invite_User() + { + var runner = new TestRunner((message, helper) => + { + //setup some mocks + Umbraco.Core.Configuration.GlobalSettings.HasSmtpServer = true; + + var userServiceMock = Mock.Get(helper.UmbracoContext.Application.Services.UserService); + + userServiceMock.Setup(service => service.Save(It.IsAny(), It.IsAny())) + .Callback((IUser u, bool raiseEvents) => + { + u.Id = 1234; + }); + userServiceMock.Setup(service => service.GetAllUserGroups(It.IsAny())) + .Returns(Enumerable.Empty); + + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + + return new UsersController(helper.UmbracoContext); + }); + + var invite = new UserInvite + { + Id = -1, + Email = "test@test.com", + Message = "Hello test!", + Name = "Test", + UserGroups = new[] {"writers"} + }; + var response = await runner.Execute("Users", "PostInviteUser", HttpMethod.Post, + new ObjectContent(invite, new JsonMediaTypeFormatter())); + + var obj = JsonConvert.DeserializeObject(response.Item2); + + Assert.AreEqual(invite.Name, obj.Name); + Assert.AreEqual(1234, obj.Id); + Assert.AreEqual(invite.Email, obj.Email); + foreach (var group in invite.UserGroups) + { + Assert.IsTrue(obj.UserGroups.Contains(group)); + } + } + [Test] public async void GetPagedUsers_Empty() { - var runner = new TestRunner((message, helper) => new UsersController(helper.UmbracoContext)); + var runner = new TestRunner((message, helper) => + { + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + + return new UsersController(helper.UmbracoContext); + }); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); var obj = JsonConvert.DeserializeObject>(response.Item2); @@ -58,6 +110,9 @@ namespace Umbraco.Tests.Controllers userServiceMock.Setup(service => service.GetAll(It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => users); + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + return new UsersController(helper.UmbracoContext); }); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 87af8f3234..fa4e851b5e 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -52,7 +52,10 @@ namespace Umbraco.Tests.TestHelpers SetupApplicationContext(); - InitializeMappers(); + if (GetType().GetCustomAttribute(false) != null) + { + InitializeMappers(ApplicationContext); + } FreezeResolution(); @@ -98,24 +101,21 @@ namespace Umbraco.Tests.TestHelpers /// This is an opt-in option because initializing the mappers takes about 500ms which equates to quite a lot /// of time with every test. /// - private void InitializeMappers() + protected virtual void InitializeMappers(ApplicationContext applicationContext) { - if (GetType().GetCustomAttribute(false) != null) + Mapper.Initialize(configuration => { - Mapper.Initialize(configuration => - { - var mappers = PluginManager.Current.FindAndCreateInstances( - specificAssemblies: new[] - { - typeof(ContentModelMapper).Assembly, - typeof(ApplicationRegistrar).Assembly - }); - foreach (var mapper in mappers) + var mappers = PluginManager.Current.FindAndCreateInstances( + specificAssemblies: new[] { - mapper.ConfigureMappings(configuration, ApplicationContext); - } - }); - } + typeof(ContentModelMapper).Assembly, + typeof(ApplicationRegistrar).Assembly + }); + foreach (var mapper in mappers) + { + mapper.ConfigureMappings(configuration, applicationContext); + } + }); } /// diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index bebf6ab1e2..7299d9758e 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -49,7 +49,9 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var serviceContext = new ServiceContext( userService: mockedUserService, - migrationEntryService: mockedMigrationService.Object); + migrationEntryService: mockedMigrationService.Object, + localizedTextService:Mock.Of(), + sectionService:Mock.Of()); //ensure the configuration matches the current version for tests SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 32e3a08e31..cc024b40a2 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting _controllerFactory = controllerFactory; } - public async Task> Execute(string controllerName, string actionName, HttpMethod method) + public async Task> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null) { var startup = new TestStartup( configuration => @@ -38,9 +38,12 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var request = new HttpRequestMessage { RequestUri = new Uri("https://testserver/"), - Method = method, + Method = method }; + if (content != null) + request.Content = content; + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); Console.WriteLine(request); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index fd70d71c99..a4260b7a49 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -401,7 +401,7 @@ namespace Umbraco.Web.Editors {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, - {"emailServerConfigured", HasSmtpServerConfigured()}, + {"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(HttpContext.Request.ApplicationPath)}, } }, { @@ -449,17 +449,7 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - private bool HasSmtpServerConfigured() - { - var config = WebConfigurationManager.OpenWebConfiguration(HttpContext.Request.ApplicationPath); - var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); - if (settings == null || settings.Smtp == null) return false; - if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) - return true; - if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) - return true; - return false; - } + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 24d1c50a65..c6ce2ecc81 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -3,15 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Web; using System.Web.Http; using AutoMapper; using ClientDependency.Core; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -95,6 +98,44 @@ namespace Umbraco.Web.Editors }; } + /// + /// Invites a user + /// + /// + /// + /// + /// This will email the user an invite and generate a token that will be validated in the email + /// + public UserDisplay PostInviteUser(UserInvite userSave) + { + if (userSave == null) throw new ArgumentNullException("userSave"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot); + if (hasSmtp == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse("No Email server is configured")); + } + + var existing = Services.UserService.GetByEmail(userSave.Email); + if (existing != null) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var user = Mapper.Map(userSave); + + Services.UserService.Save(user); + + return Mapper.Map(user); + } + /// /// Saves a user /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs new file mode 100644 index 0000000000..f3bcb80005 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Represents the data used to invite a user + /// + [DataContract(Name = "user", Namespace = "")] + public class UserInvite : EntityBasic + { + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } + + [DataMember(Name = "message", IsRequired = true)] + [Required] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 5fe57e0787..8bef19d62f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -26,9 +26,9 @@ namespace Umbraco.Web.Models.ContentEditing [EmailAddress] public string Email { get; set; } - [DataMember(Name = "userType")] + [DataMember(Name = "userGroups")] [Required] - public string UserType { get; set; } + public IEnumerable UserGroups { get; set; } [DataMember(Name = "startContentId")] public int StartContentId { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 9d1690dd9a..aa4ad4b6e4 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,6 +16,16 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + config.CreateMap() + .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) + .AfterMap((invite, user) => + { + foreach (var group in invite.UserGroups) + { + user.AddGroup(group); + } + }); + config.CreateMap() .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Sections, opt => opt.MapFrom(x => x.AllowedSections)) diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs index 832f0b3a30..5b637de3b8 100644 --- a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web.Security.Identity _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; } + //TODO: Change this - it will be different when we have user groups! private readonly string _defaultUserType; /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6666189113..6fdc3692fc 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -369,6 +369,7 @@ +