diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs new file mode 100644 index 000000000..6e531ee61 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs @@ -0,0 +1,91 @@ +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Common; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerAddressControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerAddressController( + ICustomerService customerService, + ICustomerAddressViewModelService customerAddressViewModelService, + IAddressAttributeParser addressAttributeParser, + IAddressAttributeService addressAttributeService, + IRequestTranslationService translationService, + IAdminDataScope scope) + : BaseCustomerAddressController(customerService, customerAddressViewModelService, addressAttributeParser, + addressAttributeService, translationService, scope); + + protected override TestCustomerAddressController CreateController() => new( + CustomerServiceMock.Object, + CustomerAddressViewModelServiceMock.Object, + AddressAttributeParserMock.Object, + AddressAttributeServiceMock.Object, + TranslationServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task AddressesSelect_Denied_ThrowsArgumentException() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => + Controller.AddressesSelect("missing", new DataSourceRequest())); + } + + [TestMethod] + public async Task AddressCreatePost_Authorized_InsertsAndRedirectsToAddressEdit() + { + var customer = SetupAuthorizedCustomer("c1"); + var model = new CustomerAddressModel { CustomerId = "c1", Address = new AddressModel() }; + CustomerAddressViewModelServiceMock + .Setup(v => v.InsertAddressModel(customer, model, It.IsAny>())) + .ReturnsAsync(new Address { Id = "a1" }); + + var result = await Controller.AddressCreate(model); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("AddressEdit", redirect.ActionName); + Assert.IsNull(redirect.ControllerName); + } + + [TestMethod] + public async Task AddressCreateGet_Denied_RedirectsToCustomerList() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.AddressCreate("missing"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task AddressEditGet_UnknownAddress_RedirectsToCustomerEdit() + { + SetupAuthorizedCustomer("c1"); + + var result = await Controller.AddressEdit("missing-address", "c1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Edit", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + Assert.AreEqual("c1", redirect.RouteValues!["id"]); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs index 29fcf6fce..ae96d0a24 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs @@ -1,122 +1,51 @@ -using Grand.Business.Core.Interfaces.Common.Addresses; -using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Domain.Customers; using Grand.Infrastructure; -using Grand.Infrastructure.Configuration; using Grand.Web.AdminShared.Controllers; -using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Common; using Grand.Web.AdminShared.Models.Customers; using Grand.Web.Common.DataSource; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Grand.Web.Common.Localization; -using Grand.Web.Common.Security.Authorization; namespace Grand.Web.Admin.Tests.Controllers; [TestClass] -public class BaseCustomerControllerTests +public class BaseCustomerControllerTests : CustomerControllerTestsBase { // BaseCustomerController is abstract; minimal subclass so actions can be invoked directly. - // The full 17-dependency constructor is declared once here; every later task's tests reuse - // this same TestCustomerController and Setup(). - protected class TestCustomerController( - ICustomerService customerService, - ICustomerViewModelService customerViewModelService, - ICustomerManagerService customerManagerService, - ICustomerProductService customerProductService, - Grand.Business.Core.Interfaces.Catalog.Products.IProductReviewService productReviewService, - Grand.Web.AdminShared.Interfaces.IProductReviewViewModelService productReviewViewModelService, - Grand.Web.AdminShared.Interfaces.IProductViewModelService productViewModelService, - ICustomerAttributeParser customerAttributeParser, - ICustomerAttributeService customerAttributeService, - IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, + public class TestCustomerController( + Grand.Business.Core.Interfaces.Customers.ICustomerService customerService, + Grand.Web.AdminShared.Interfaces.ICustomerViewModelService customerViewModelService, + Grand.Business.Core.Interfaces.Customers.ICustomerManagerService customerManagerService, + Grand.Business.Core.Interfaces.Customers.ICustomerAttributeParser customerAttributeParser, + Grand.Business.Core.Interfaces.Customers.ICustomerAttributeService customerAttributeService, Grand.Business.Core.Interfaces.Messages.IMessageProviderService messageProviderService, - IGroupService groupService, - IRequestTranslationService translationService, + Grand.Business.Core.Interfaces.Common.Directory.IGroupService groupService, + Grand.Web.Common.Localization.IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, - IAdminDataScope scope) + Grand.Web.AdminShared.Interfaces.IAdminDataScope scope) : BaseCustomerController(customerService, customerViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, - productViewModelService, customerAttributeParser, customerAttributeService, - addressAttributeParser, addressAttributeService, messageProviderService, groupService, + customerAttributeParser, customerAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) { public Task<(Customer, IActionResult)> LoadAuthorizedCustomerPublic(string id) => LoadAuthorizedCustomer(id); } - protected TestCustomerController Controller; - protected Mock CustomerServiceMock; - protected Mock CustomerViewModelServiceMock; - protected Mock CustomerManagerServiceMock; - protected Mock GroupServiceMock; - protected Mock> ScopeMock; - protected Mock TranslationServiceMock; - protected Mock AddressAttributeServiceMock; - protected Mock ProductReviewServiceMock; - protected Mock CustomerProductServiceMock; - - [TestInitialize] - public void Setup() - { - CustomerServiceMock = new Mock(); - CustomerViewModelServiceMock = new Mock(); - CustomerManagerServiceMock = new Mock(); - GroupServiceMock = new Mock(); - ScopeMock = new Mock>(); - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - TranslationServiceMock = new Mock(); - TranslationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); - AddressAttributeServiceMock = new Mock(); - AddressAttributeServiceMock.Setup(a => a.GetAllAddressAttributes()) - .ReturnsAsync(new List()); - ProductReviewServiceMock = new Mock(); - CustomerProductServiceMock = new Mock(); - - var contextAccessorMock = new Mock(); - - Controller = new TestCustomerController( - CustomerServiceMock.Object, - CustomerViewModelServiceMock.Object, - CustomerManagerServiceMock.Object, - CustomerProductServiceMock.Object, - ProductReviewServiceMock.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - AddressAttributeServiceMock.Object, - new Mock().Object, - GroupServiceMock.Object, - TranslationServiceMock.Object, - contextAccessorMock.Object, - new CustomerSettings(), - ScopeMock.Object); - - var httpContext = new DefaultHttpContext(); - var loggerFactoryMock = new Mock(); - loggerFactoryMock.Setup(l => l.CreateLogger(It.IsAny())).Returns(new Mock().Object); - var urlHelperFactoryMock = new Mock(); - urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny())).Returns(new Mock().Object); - var requestServicesMock = new Mock(); - requestServicesMock.Setup(s => s.GetService(typeof(ILoggerFactory))).Returns(loggerFactoryMock.Object); - requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object); - httpContext.RequestServices = requestServicesMock.Object; - Controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; - Controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); - } + protected override TestCustomerController CreateController() => new( + CustomerServiceMock.Object, + CustomerViewModelServiceMock.Object, + CustomerManagerServiceMock.Object, + CustomerAttributeParserMock.Object, + CustomerAttributeServiceMock.Object, + MessageProviderServiceMock.Object, + GroupServiceMock.Object, + TranslationServiceMock.Object, + ContextAccessorMock.Object, + new CustomerSettings(), + ScopeMock.Object); + [TestMethod] public async Task List_CallsPrepareCustomerListModel_ForBothHosts() @@ -289,6 +218,7 @@ public async Task LoadAuthorizedCustomer_NotFound_ReturnsRedirectToList() var redirect = denied as RedirectToActionResult; Assert.IsNotNull(redirect); Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); ScopeMock.Verify(s => s.HasAccess(It.IsAny()), Times.Never); } @@ -349,29 +279,13 @@ public async Task EditGet_Denied_RedirectsToList() public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() { var self = new Customer { Id = "self" }; - var contextAccessorMock = new Mock(); var workContextMock = new Mock(); workContextMock.Setup(w => w.CurrentCustomer).Returns(self); - contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); - // Rebuild controller with a context accessor whose CurrentCustomer matches the target id. - var controller = new TestCustomerController(CustomerServiceMock.Object, CustomerViewModelServiceMock.Object, - CustomerManagerServiceMock.Object, new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, - GroupServiceMock.Object, TranslationServiceMock.Object, contextAccessorMock.Object, - new CustomerSettings(), ScopeMock.Object) - { - ControllerContext = Controller.ControllerContext, - TempData = Controller.TempData - }; + ContextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); CustomerServiceMock.Setup(s => s.GetCustomerById("self")).ReturnsAsync(self); ScopeMock.Setup(s => s.HasAccess(self)).ReturnsAsync(true); - var result = await controller.Delete("self"); + var result = await Controller.Delete("self"); var redirect = result as RedirectToActionResult; Assert.IsNotNull(redirect); @@ -379,296 +293,6 @@ public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() CustomerViewModelServiceMock.Verify(v => v.DeleteCustomer(It.IsAny()), Times.Never); } - [TestMethod] - public async Task LoyaltyPointsHistoryAdd_StoreScope_ForcesStaffStoreId_IgnoringSubmittedStoreId() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerViewModelServiceMock - .Setup(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg")) - .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); - - // Caller submits a different, attacker-controlled storeId — must be ignored under Store scope. - await Controller.LoyaltyPointsHistoryAdd("c1", "attacker-store", 10, "msg"); - - CustomerViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); - } - - [TestMethod] - public async Task LoyaltyPointsHistoryAdd_GlobalScope_UsesSubmittedStoreId() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock - .Setup(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg")) - .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); - - await Controller.LoyaltyPointsHistoryAdd("c1", "admin-submitted-store", 10, "msg"); - - CustomerViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); - } - - [TestMethod] - public async Task AddressesSelect_Denied_ThrowsArgumentException() - { - CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); - - await Assert.ThrowsAsync(() => - Controller.AddressesSelect("missing", new DataSourceRequest())); - } - - [TestMethod] - public async Task AddressCreatePost_Authorized_InsertsAndRedirects() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - var model = new CustomerAddressModel { CustomerId = "c1", Address = new AddressModel() }; - var inserted = new Grand.Domain.Common.Address { Id = "a1" }; - CustomerViewModelServiceMock - .Setup(v => v.InsertAddressModel(customer, model, It.IsAny>())) - .ReturnsAsync(inserted); - - var result = await Controller.AddressCreate(model); - - var redirect = result as RedirectToActionResult; - Assert.IsNotNull(redirect); - Assert.AreEqual("AddressEdit", redirect.ActionName); - } - - [TestMethod] - public async Task OrderDetails_StoreScope_MismatchedStore_ReturnsEmptyNotException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var orderServiceMock = new Mock(); - orderServiceMock.Setup(s => s.GetOrderById("o1")) - .ReturnsAsync(new Grand.Domain.Orders.Order { Id = "o1", StoreId = "store-2" }); - - var result = await Controller.OrderDetails("o1", orderServiceMock.Object, - new Mock().Object, - new Mock().Object); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - } - - [TestMethod] - public async Task OrderDetails_GlobalScope_NotFound_ThrowsArgumentException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - var permissionServiceMock = new Mock(); - permissionServiceMock.Setup(p => p.Authorize(Grand.Domain.Permissions.StandardPermission.ManageOrders)).ReturnsAsync(true); - var orderServiceMock = new Mock(); - orderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Grand.Domain.Orders.Order)null); - - await Assert.ThrowsAsync(() => - Controller.OrderDetails("missing", orderServiceMock.Object, - new Mock().Object, permissionServiceMock.Object)); - } - - [TestMethod] - public async Task GetCartList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel("any-customer-id", 1)) - .ReturnsAsync(new List()); - - var result = await Controller.GetCartList("any-customer-id", 1); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task GetCartList_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.GetCartList("c1", 1); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify(v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ReviewDelete_StoreScope_MismatchedStore_ThrowsArgumentException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var review = new Grand.Domain.Catalog.ProductReview { Id = "r1", StoreId = "store-2" }; - ProductReviewServiceMock.Setup(s => s.GetProductReviewById("r1")).ReturnsAsync(review); - - await Assert.ThrowsAsync(() => Controller.ReviewDelete("r1")); - } - - [TestMethod] - public async Task DeleteProductPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")) - .ReturnsAsync((Grand.Domain.Customers.CustomerProductPrice)null); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); - CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently() - { - // Price exists but belongs to a customer outside the caller's store — exercises the - // ownership-denial half of the guard, distinct from the not-found half above. - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var price = new Grand.Domain.Customers.CustomerProductPrice { Id = "pp1", CustomerId = "c1" }; - CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - var foreignCustomer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(foreignCustomer); - ScopeMock.Setup(s => s.HasAccess(foreignCustomer)).ReturnsAsync(false); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); - CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ProductAddPopup_Get_StoreScope_Denied_RedirectsToList() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ProductAddPopup("c1"); - - var redirect = result as RedirectToActionResult; - Assert.IsNotNull(redirect); - Assert.AreEqual("List", redirect.ActionName); - } - - [TestMethod] - public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify( - v => v.PrepareProductPriceModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ProductsPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); - } - - [TestMethod] - public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify( - v => v.PreparePersonalizedProducts(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task PersonalizedProducts_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); - } - - [TestMethod] - public async Task ReviewList_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - ProductReviewServiceMock.Verify(s => s.GetAllProductReviews( - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ReviewList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - ProductReviewServiceMock.Setup(s => s.GetAllProductReviews( - "c1", null, null, null, "", null, "", 0, 10)) - .ReturnsAsync(new Grand.Domain.PagedList( - new List(), 0, 10, 0)); - - var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task OutOfStockSubscriptionList_GlobalScope_NoOwnershipCheck() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock - .Setup(v => v.PrepareOutOfStockSubscriptionModel("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.OutOfStockSubscriptionList( - new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } [TestMethod] public async Task ExternalAuthenticationDelete_CustomerInScope_RemovesTheLink() diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs new file mode 100644 index 000000000..be60c78a4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs @@ -0,0 +1,62 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerLoyaltyPointsControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerLoyaltyPointsController( + ICustomerService customerService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + IAdminDataScope scope) + : BaseCustomerLoyaltyPointsController(customerService, customerLoyaltyPointsViewModelService, scope); + + protected override TestCustomerLoyaltyPointsController CreateController() => new( + CustomerServiceMock.Object, + CustomerLoyaltyPointsViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task LoyaltyPointsHistoryAdd_StoreScope_ForcesStaffStoreId_IgnoringSubmittedStoreId() + { + var customer = SetupAuthorizedCustomer("c1"); + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerLoyaltyPointsViewModelServiceMock + .Setup(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg")) + .ReturnsAsync(new LoyaltyPointsHistory()); + + // Caller submits a different, attacker-controlled storeId — must be ignored under Store scope. + await Controller.LoyaltyPointsHistoryAdd("c1", "attacker-store", 10, "msg"); + + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); + } + + [TestMethod] + public async Task LoyaltyPointsHistoryAdd_GlobalScope_UsesSubmittedStoreId() + { + var customer = SetupAuthorizedCustomer("c1"); + CustomerLoyaltyPointsViewModelServiceMock + .Setup(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg")) + .ReturnsAsync(new LoyaltyPointsHistory()); + + await Controller.LoyaltyPointsHistoryAdd("c1", "admin-submitted-store", 10, "msg"); + + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); + } + + [TestMethod] + public async Task LoyaltyPointsHistorySelect_Denied_ThrowsArgumentException() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => Controller.LoyaltyPointsHistorySelect("missing")); + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.PrepareLoyaltyPointsHistoryModel(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs index 7fd6b3167..43272de19 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs @@ -1,9 +1,6 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.ExportImport; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Customers; using Grand.Domain.Permissions; @@ -29,19 +26,14 @@ public class BaseCustomerManagementControllerTests { private class TestManagementController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, - ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, - IProductReviewService productReviewService, IProductReviewViewModelService productReviewViewModelService, - IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, - ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IMessageProviderService messageProviderService, + ICustomerManagerService customerManagerService, ICustomerAttributeParser customerAttributeParser, + ICustomerAttributeService customerAttributeService, IMessageProviderService messageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, IAdminDataScope scope, IRequestPermissionService permissionService, IExportManager exportManager) : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, productViewModelService, - customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, - permissionService, exportManager) + customerAttributeParser, customerAttributeService, messageProviderService, groupService, + translationService, contextAccessor, customerSettings, scope, permissionService, exportManager) { public void CheckTwoFactorEnabledWarningPublic(Customer existingCustomer, CustomerModel model) => CheckTwoFactorEnabledWarning(existingCustomer, model); @@ -71,10 +63,7 @@ public void Setup() _controller = new TestManagementController(_customerServiceMock.Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, new Mock().Object, _groupServiceMock.Object, translationServiceMock.Object, _contextAccessorMock.Object, new CustomerSettings(), scopeMock.Object, _permissionServiceMock.Object, new Mock>().Object); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs new file mode 100644 index 000000000..01356e49e --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs @@ -0,0 +1,69 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerOrderControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerOrderController( + ICustomerService customerService, + IOrderService orderService, + IOrderViewModelService orderViewModelService, + IRequestPermissionService permissionService, + IAdminDataScope scope) + : BaseCustomerOrderController(customerService, orderService, orderViewModelService, permissionService, scope); + + protected override TestCustomerOrderController CreateController() => new( + CustomerServiceMock.Object, + OrderServiceMock.Object, + OrderViewModelServiceMock.Object, + PermissionServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task OrderDetails_StoreScope_MismatchedStore_ReturnsEmptyNotException() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + OrderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(new Order { Id = "o1", StoreId = "store-2" }); + + var result = await Controller.OrderDetails("o1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + } + + [TestMethod] + public async Task OrderDetails_GlobalScope_NotFound_ThrowsArgumentException() + { + PermissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(true); + OrderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Order)null); + + await Assert.ThrowsAsync(() => Controller.OrderDetails("missing")); + } + + [TestMethod] + public async Task OrderList_GlobalScope_WithoutManageOrders_ReturnsEmpty() + { + PermissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(false); + + var result = await Controller.OrderList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + OrderViewModelServiceMock.Verify( + v => v.PrepareOrderModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs new file mode 100644 index 000000000..5ae489e23 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs @@ -0,0 +1,55 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerOutOfStockSubscriptionControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerOutOfStockSubscriptionController( + ICustomerService customerService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, + IAdminDataScope scope) + : BaseCustomerOutOfStockSubscriptionController(customerService, customerOutOfStockSubscriptionViewModelService, + scope); + + protected override TestCustomerOutOfStockSubscriptionController CreateController() => new( + CustomerServiceMock.Object, + CustomerOutOfStockSubscriptionViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task OutOfStockSubscriptionList_GlobalScope_NoOwnershipCheck() + { + CustomerOutOfStockSubscriptionViewModelServiceMock + .Setup(v => v.PrepareOutOfStockSubscriptionModel("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.OutOfStockSubscriptionList(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task OutOfStockSubscriptionList_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.OutOfStockSubscriptionList(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerOutOfStockSubscriptionViewModelServiceMock.Verify( + v => v.PrepareOutOfStockSubscriptionModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs new file mode 100644 index 000000000..7d043be89 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs @@ -0,0 +1,142 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerProductControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerProductController( + ICustomerService customerService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerProductService customerProductService, + IAdminDataScope scope) + : BaseCustomerProductController(customerService, customerProductViewModelService, customerProductService, scope); + + protected override TestCustomerProductController CreateController() => new( + CustomerServiceMock.Object, + CustomerProductViewModelServiceMock.Object, + CustomerProductServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task DeleteProductPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")) + .ReturnsAsync((CustomerProductPrice)null); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently() + { + // Price exists but belongs to a customer outside the caller's store — exercises the + // ownership-denial half of the guard, distinct from the not-found half above. + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + var price = new CustomerProductPrice { Id = "pp1", CustomerId = "c1" }; + CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + var foreignCustomer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(foreignCustomer); + ScopeMock.Setup(s => s.HasAccess(foreignCustomer)).ReturnsAsync(false); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ProductAddPopup_Get_StoreScope_Denied_RedirectsToCustomerList() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ProductAddPopup("c1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerProductViewModelServiceMock.Verify( + v => v.PrepareProductPriceModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ProductsPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerProductViewModelServiceMock.Verify( + v => v.PreparePersonalizedProducts(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task PersonalizedProducts_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs new file mode 100644 index 000000000..a2256a1d4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs @@ -0,0 +1,72 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerReviewControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerReviewController( + ICustomerService customerService, + IProductReviewService productReviewService, + IProductReviewViewModelService productReviewViewModelService, + IProductViewModelService productViewModelService, + IAdminDataScope scope) + : BaseCustomerReviewController(customerService, productReviewService, productReviewViewModelService, + productViewModelService, scope); + + protected override TestCustomerReviewController CreateController() => new( + CustomerServiceMock.Object, + ProductReviewServiceMock.Object, + ProductReviewViewModelServiceMock.Object, + ProductViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task ReviewDelete_StoreScope_MismatchedStore_ThrowsArgumentException() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + ProductReviewServiceMock.Setup(s => s.GetProductReviewById("r1")) + .ReturnsAsync(new ProductReview { Id = "r1", StoreId = "store-2" }); + + await Assert.ThrowsAsync(() => Controller.ReviewDelete("r1")); + ProductReviewViewModelServiceMock.Verify(v => v.DeleteProductReview(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ReviewList_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + ProductReviewServiceMock.Verify(s => s.GetAllProductReviews( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ReviewList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + ProductReviewServiceMock.Setup(s => s.GetAllProductReviews("c1", null, null, null, "", null, "", 0, 10)) + .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); + + var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs new file mode 100644 index 000000000..5adfbd547 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs @@ -0,0 +1,79 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.ShoppingCart; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerShoppingCartControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerShoppingCartController( + ICustomerService customerService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + IAdminDataScope scope) + : BaseCustomerShoppingCartController(customerService, customerShoppingCartViewModelService, scope); + + protected override TestCustomerShoppingCartController CreateController() => new( + CustomerServiceMock.Object, + CustomerShoppingCartViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task GetCartList_Authorized_PassesTheLoadedCustomerToTheService() + { + var customer = SetupAuthorizedCustomer("c1"); + CustomerShoppingCartViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel(customer, 1)) + .ReturnsAsync(new List { new() }); + + var result = await Controller.GetCartList("c1", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(1, data.Total); + CustomerServiceMock.Verify(s => s.GetCustomerById("c1"), Times.Once); + } + + [TestMethod] + public async Task GetCartList_GlobalScope_UnknownCustomer_ReturnsEmptyGrid() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.GetCartList("missing", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerShoppingCartViewModelServiceMock.Verify( + v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task GetCartList_StoreScope_DeniedOwnership_ReturnsEmptyGrid() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + var customer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(false); + + var result = await Controller.GetCartList("c1", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerShoppingCartViewModelServiceMock.Verify( + v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task DeleteCart_Denied_ThrowsArgumentException_NothingDeleted() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => Controller.DeleteCart("sci1", "missing")); + CustomerShoppingCartViewModelServiceMock.Verify(v => v.DeleteCart(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs index 0013b8893..b5f4c1113 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs @@ -93,4 +93,29 @@ public void CreatePost_RequiresCreatePermission() Assert.AreEqual(PermissionActionName.Create, attr.PermissionAction); } + + [TestMethod] + public void CustomerTabControllers_AllCarryAdminAreaAndAuthorization() + { + Type[] tabControllers = [ + typeof(CustomerAddressController), + typeof(CustomerLoyaltyPointsController), + typeof(CustomerShoppingCartController), + typeof(CustomerProductController), + typeof(CustomerOutOfStockSubscriptionController), + typeof(CustomerOrderController), + typeof(CustomerReviewController), + typeof(CustomerNoteController), + typeof(CustomerContactFormController) + ]; + foreach (var type in tabControllers) + { + var area = type.GetCustomAttributes(typeof(AreaAttribute), inherit: false).Cast().Single(); + Assert.AreEqual("Admin", area.RouteValue, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeAdminAttribute), inherit: false).Length, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false).Length, type.Name); + // [PermissionAuthorize(Customers)] and antiforgery arrive from BaseCustomerScopedController. + Assert.IsTrue(typeof(BaseCustomerScopedController).IsAssignableFrom(type), type.Name); + } + } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs new file mode 100644 index 000000000..ea932900c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs @@ -0,0 +1,114 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.Localization; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +/// +/// Fixture shared by the tests of BaseCustomerController and the per-tab customer controllers split out +/// of it. Each derived class only says how to build its controller from the mocks below. +/// +public abstract class CustomerControllerTestsBase where TController : Controller +{ + protected TController Controller; + protected Mock CustomerServiceMock; + protected Mock CustomerViewModelServiceMock; + protected Mock CustomerAddressViewModelServiceMock; + protected Mock CustomerLoyaltyPointsViewModelServiceMock; + protected Mock CustomerShoppingCartViewModelServiceMock; + protected Mock CustomerProductViewModelServiceMock; + protected Mock CustomerOutOfStockSubscriptionViewModelServiceMock; + protected Mock CustomerNoteViewModelServiceMock; + protected Mock CustomerManagerServiceMock; + protected Mock CustomerProductServiceMock; + protected Mock ProductReviewServiceMock; + protected Mock ProductReviewViewModelServiceMock; + protected Mock ProductViewModelServiceMock; + protected Mock OrderServiceMock; + protected Mock OrderViewModelServiceMock; + protected Mock PermissionServiceMock; + protected Mock CustomerAttributeParserMock; + protected Mock CustomerAttributeServiceMock; + protected Mock AddressAttributeParserMock; + protected Mock AddressAttributeServiceMock; + protected Mock MessageProviderServiceMock; + protected Mock GroupServiceMock; + protected Mock TranslationServiceMock; + protected Mock ContextAccessorMock; + protected Mock> ScopeMock; + + protected abstract TController CreateController(); + + [TestInitialize] + public void Setup() + { + CustomerServiceMock = new Mock(); + CustomerViewModelServiceMock = new Mock(); + CustomerAddressViewModelServiceMock = new Mock(); + CustomerLoyaltyPointsViewModelServiceMock = new Mock(); + CustomerShoppingCartViewModelServiceMock = new Mock(); + CustomerProductViewModelServiceMock = new Mock(); + CustomerOutOfStockSubscriptionViewModelServiceMock = new Mock(); + CustomerNoteViewModelServiceMock = new Mock(); + CustomerManagerServiceMock = new Mock(); + CustomerProductServiceMock = new Mock(); + ProductReviewServiceMock = new Mock(); + ProductReviewViewModelServiceMock = new Mock(); + ProductViewModelServiceMock = new Mock(); + OrderServiceMock = new Mock(); + OrderViewModelServiceMock = new Mock(); + PermissionServiceMock = new Mock(); + CustomerAttributeParserMock = new Mock(); + CustomerAttributeServiceMock = new Mock(); + AddressAttributeParserMock = new Mock(); + AddressAttributeServiceMock = new Mock(); + AddressAttributeServiceMock.Setup(a => a.GetAllAddressAttributes()) + .ReturnsAsync(new List()); + MessageProviderServiceMock = new Mock(); + GroupServiceMock = new Mock(); + TranslationServiceMock = new Mock(); + TranslationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + ContextAccessorMock = new Mock(); + ScopeMock = new Mock>(); + ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + + Controller = CreateController(); + + var httpContext = new DefaultHttpContext(); + var loggerFactoryMock = new Mock(); + loggerFactoryMock.Setup(l => l.CreateLogger(It.IsAny())).Returns(new Mock().Object); + var urlHelperFactoryMock = new Mock(); + urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny())).Returns(new Mock().Object); + var requestServicesMock = new Mock(); + requestServicesMock.Setup(s => s.GetService(typeof(ILoggerFactory))).Returns(loggerFactoryMock.Object); + requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object); + httpContext.RequestServices = requestServicesMock.Object; + Controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + Controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + } + + /// Makes customerId resolve to a customer the scope grants access to. + protected Customer SetupAuthorizedCustomer(string customerId) + { + var customer = new Customer { Id = customerId }; + CustomerServiceMock.Setup(s => s.GetCustomerById(customerId)).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); + return customer; + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs new file mode 100644 index 000000000..6655a2435 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs @@ -0,0 +1,51 @@ +using Grand.Domain.Customers; +using Grand.Web.Admin.Controllers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class CustomerNoteControllerTests : CustomerControllerTestsBase +{ + protected override CustomerNoteController CreateController() => new( + CustomerServiceMock.Object, + CustomerNoteViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task CustomerNoteDelete_CustomerOutOfScope_Throws_NothingDeleted() + { + var customer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(false); + + await Assert.ThrowsAsync(() => Controller.CustomerNoteDelete("n1", "c1")); + CustomerNoteViewModelServiceMock.Verify(v => v.DeleteCustomerNote(It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task CustomerNoteDelete_CustomerInScope_DeletesWithinThatCustomer() + { + SetupAuthorizedCustomer("c1"); + + var result = await Controller.CustomerNoteDelete("n1", "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerNoteViewModelServiceMock.Verify(v => v.DeleteCustomerNote("n1", "c1"), Times.Once); + } + + [TestMethod] + public async Task CustomerNoteAdd_CustomerOutOfScope_ReturnsFalse_NothingInserted() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.CustomerNoteAdd("missing", "", false, "title", "message"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerNoteViewModelServiceMock.Verify(v => v.InsertCustomerNote(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs new file mode 100644 index 000000000..2f3857280 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs @@ -0,0 +1,49 @@ +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerAddressViewModelServiceTests +{ + private Mock _customerServiceMock; + private CustomerAddressViewModelService _customerAddressViewModelService; + + [TestInitialize] + public void Setup() + { + _customerServiceMock = new Mock(); + _customerServiceMock.Setup(c => c.UpdateCustomerInAdminPanel(It.IsAny())).Returns(Task.CompletedTask); + + _customerAddressViewModelService = new CustomerAddressViewModelService( + _customerServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new AddressSettings()); + } + + [TestMethod] + public async Task DeleteAddress_RemovesAddressAndUpdatesCustomer() + { + var address = new Address { Id = "a1" }; + var customer = new Customer(); + customer.Addresses.Add(address); + + await _customerAddressViewModelService.DeleteAddress(customer, address); + + Assert.IsFalse(customer.Addresses.Any(a => a.Id == "a1")); + _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs new file mode 100644 index 000000000..7c5faa0a1 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs @@ -0,0 +1,72 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Domain; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Infrastructure.Mapper; +using Grand.Mapping; +using Grand.Web.AdminShared.Mapper; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerContactFormViewModelServiceTests +{ + private Mock _contactUsServiceMock; + private Mock _storeServiceMock; + private CustomerContactFormViewModelService _service; + + [TestInitialize] + public void Setup() + { + AutoMapperConfig.Init(new MapperConfiguration(cfg => cfg.AddProfile())); + + _contactUsServiceMock = new Mock(); + _storeServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), DateTimeKind.Utc)) + .Returns((d, _) => d); + + _service = new CustomerContactFormViewModelService(_contactUsServiceMock.Object, _storeServiceMock.Object, + dateTimeServiceMock.Object); + } + + [TestMethod] + public async Task PrepareContactFormModel_QueriesTheCustomersFormsWithZeroBasedPage() + { + _contactUsServiceMock.Setup(c => c.GetAllContactUs(null, null, "", "", "c1", "", 1, 10)) + .ReturnsAsync(new PagedList(new List(), 1, 10, 0)); + + var (models, totalCount) = await _service.PrepareContactFormModel("c1", 2, 10); + + Assert.AreEqual(0, models.Count()); + Assert.AreEqual(0, totalCount); + _contactUsServiceMock.Verify(c => c.GetAllContactUs(null, null, "", "", "c1", "", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PrepareContactFormModel_HidesTheEnquiry_AndNamesTheStore() + { + var known = new ContactUs { + Id = "f1", StoreId = "s1", FullName = "Jan", Email = "jan@example.com", Enquiry = "secret" + }; + var orphan = new ContactUs { Id = "f2", StoreId = "gone", FullName = "Ewa", Email = "ewa@example.com" }; + _contactUsServiceMock.Setup(c => c.GetAllContactUs(null, null, "", "", "c1", "", 0, 10)) + .ReturnsAsync(new PagedList(new List { known, orphan }, 0, 10, 2)); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1", Shortcut = "Main" }); + _storeServiceMock.Setup(s => s.GetStoreById("gone")).ReturnsAsync((Store)null); + + var (models, totalCount) = await _service.PrepareContactFormModel("c1", 1, 10); + + var list = models.ToList(); + Assert.AreEqual(2, totalCount); + Assert.AreEqual("", list[0].Enquiry); + Assert.AreEqual("Jan - jan@example.com", list[0].Email); + Assert.AreEqual("Main", list[0].Store); + Assert.AreEqual("-empty-", list[1].Store); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs new file mode 100644 index 000000000..981f99ece --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs @@ -0,0 +1,69 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Services; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerLoyaltyPointsViewModelServiceTests +{ + private Mock _loyaltyPointsServiceMock; + private Mock _storeServiceMock; + private CustomerLoyaltyPointsViewModelService _customerLoyaltyPointsViewModelService; + + [TestInitialize] + public void Setup() + { + _loyaltyPointsServiceMock = new Mock(); + _storeServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), It.IsAny())) + .Returns((dt, _) => dt); + + _customerLoyaltyPointsViewModelService = new CustomerLoyaltyPointsViewModelService( + _loyaltyPointsServiceMock.Object, + _storeServiceMock.Object, + dateTimeServiceMock.Object); + } + + [TestMethod] + public async Task PrepareLoyaltyPointsHistoryModel_MapsHistory() + { + _loyaltyPointsServiceMock + .Setup(l => l.GetLoyaltyPointsHistory(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { + new() { StoreId = "store-1", Points = 5, PointsBalance = 10, Message = "added" } + }); + _storeServiceMock.Setup(s => s.GetStoreById("store-1")) + .ReturnsAsync(new Grand.Domain.Stores.Store { Id = "store-1", Shortcut = "Store 1" }); + + var result = (await _customerLoyaltyPointsViewModelService.PrepareLoyaltyPointsHistoryModel("c1")).ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Store 1", result[0].StoreName); + Assert.AreEqual(5, result[0].Points); + } + + [TestMethod] + public async Task InsertLoyaltyPointsHistory_DelegatesToService() + { + var history = new LoyaltyPointsHistory(); + _loyaltyPointsServiceMock + .Setup(l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny())) + .ReturnsAsync(history); + + var result = await _customerLoyaltyPointsViewModelService.InsertLoyaltyPointsHistory( + new Customer { Id = "c1" }, "store-1", 10, "msg"); + + Assert.AreSame(history, result); + _loyaltyPointsServiceMock.Verify( + l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny()), + Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs new file mode 100644 index 000000000..f38f7afcd --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs @@ -0,0 +1,91 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Customers; +using Grand.Domain.Media; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerNoteViewModelServiceTests +{ + private Mock _customerNoteServiceMock; + private Mock _downloadServiceMock; + private CustomerNoteViewModelService _customerNoteViewModelService; + + [TestInitialize] + public void Setup() + { + _customerNoteServiceMock = new Mock(); + _downloadServiceMock = new Mock(); + _customerNoteServiceMock.Setup(c => c.InsertCustomerNote(It.IsAny())) + .Returns(Task.CompletedTask); + + _customerNoteViewModelService = new CustomerNoteViewModelService( + _customerNoteServiceMock.Object, + new Mock().Object, + _downloadServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object); + } + + [TestMethod] + public async Task InsertCustomerNote_InsertsNoteWithMappedFields() + { + var note = await _customerNoteViewModelService.InsertCustomerNote("c1", "d1", false, "title", "message"); + + Assert.AreEqual("c1", note.CustomerId); + Assert.AreEqual("d1", note.DownloadId); + Assert.AreEqual("title", note.Title); + Assert.AreEqual("message", note.Note); + Assert.IsFalse(note.DisplayToCustomer); + _customerNoteServiceMock.Verify( + c => c.InsertCustomerNote(It.Is(x => x.CustomerId == "c1" && x.Title == "title")), + Times.Once); + } + + [TestMethod] + public async Task DeleteCustomerNote_DeletesNoteAndAttachment() + { + var note = new CustomerNote { Id = "n1", CustomerId = "c1", DownloadId = "d1" }; + var download = new Download { Id = "d1" }; + _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); + _customerNoteServiceMock.Setup(c => c.DeleteCustomerNote(It.IsAny())).Returns(Task.CompletedTask); + _downloadServiceMock.Setup(d => d.GetDownloadById("d1")).ReturnsAsync(download); + _downloadServiceMock.Setup(d => d.DeleteDownload(It.IsAny())).Returns(Task.CompletedTask); + + await _customerNoteViewModelService.DeleteCustomerNote("n1", "c1"); + + _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(note), Times.Once); + _downloadServiceMock.Verify(d => d.DeleteDownload(download), Times.Once); + } + + [TestMethod] + public async Task DeleteCustomerNote_NotFound_Throws() + { + _customerNoteServiceMock.Setup(c => c.GetCustomerNote(It.IsAny())) + .ReturnsAsync((CustomerNote)null); + + await Assert.ThrowsAsync( + () => _customerNoteViewModelService.DeleteCustomerNote("missing", "c1")); + } + + [TestMethod] + public async Task DeleteCustomerNote_NoteOfAnotherCustomer_Throws_NothingDeleted() + { + var note = new CustomerNote { Id = "n1", CustomerId = "c2", DownloadId = "d1" }; + _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); + + await Assert.ThrowsAsync( + () => _customerNoteViewModelService.DeleteCustomerNote("n1", "c1")); + _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(It.IsAny()), Times.Never); + _downloadServiceMock.Verify(d => d.DeleteDownload(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs new file mode 100644 index 000000000..dbd8807ff --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs @@ -0,0 +1,75 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerOutOfStockSubscriptionViewModelServiceTests +{ + private Mock _outOfStockSubscriptionServiceMock; + private Mock _storeServiceMock; + private Mock _productServiceMock; + private Mock _productAttributeFormatterMock; + private CustomerOutOfStockSubscriptionViewModelService _service; + + [TestInitialize] + public void Setup() + { + _outOfStockSubscriptionServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _productServiceMock = new Mock(); + _productAttributeFormatterMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), DateTimeKind.Utc)) + .Returns((d, _) => d); + + _service = new CustomerOutOfStockSubscriptionViewModelService(_outOfStockSubscriptionServiceMock.Object, + _storeServiceMock.Object, _productServiceMock.Object, _productAttributeFormatterMock.Object, + dateTimeServiceMock.Object, new Mock().Object); + } + + [TestMethod] + public async Task PrepareOutOfStockSubscriptionModel_QueriesWithZeroBasedPage() + { + _outOfStockSubscriptionServiceMock.Setup(s => s.GetAllSubscriptionsByCustomerId("c1", "", 1, 10)) + .ReturnsAsync(new PagedList(new List(), 1, 10, 0)); + + var (models, totalCount) = await _service.PrepareOutOfStockSubscriptionModel("c1", 2, 10); + + Assert.AreEqual(0, models.Count()); + Assert.AreEqual(0, totalCount); + _outOfStockSubscriptionServiceMock.Verify(s => s.GetAllSubscriptionsByCustomerId("c1", "", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PrepareOutOfStockSubscriptionModel_MapsStoreAndProduct_UnknownWhenMissing() + { + var subscriptions = new List { + new() { Id = "o1", StoreId = "s1", ProductId = "p1" }, + new() { Id = "o2", StoreId = "gone", ProductId = "gone" } + }; + _outOfStockSubscriptionServiceMock.Setup(s => s.GetAllSubscriptionsByCustomerId("c1", "", 0, 10)) + .ReturnsAsync(new PagedList(subscriptions, 0, 10, 2)); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1", Shortcut = "Main" }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(new Product { Id = "p1", Name = "Mug" }); + + var (models, totalCount) = await _service.PrepareOutOfStockSubscriptionModel("c1", 1, 10); + + var list = models.ToList(); + Assert.AreEqual(2, totalCount); + Assert.AreEqual("Main", list[0].StoreName); + Assert.AreEqual("Mug", list[0].ProductName); + Assert.AreEqual("", list[0].AttributeDescription); + Assert.AreEqual("Unknown", list[1].StoreName); + Assert.AreEqual("Unknown", list[1].ProductName); + _productAttributeFormatterMock.VerifyNoOtherCalls(); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs new file mode 100644 index 000000000..518505634 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs @@ -0,0 +1,201 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerProductViewModelServiceTests +{ + private Mock _customerProductServiceMock; + private Mock _productServiceMock; + private Mock _storeServiceMock; + private Mock _vendorServiceMock; + private Mock _enumTranslationServiceMock; + private CustomerProductViewModelService _customerProductViewModelService; + + [TestInitialize] + public void Setup() + { + _customerProductServiceMock = new Mock(); + _productServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _vendorServiceMock = new Mock(); + _enumTranslationServiceMock = new Mock(); + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns(k => k); + + _customerProductViewModelService = new CustomerProductViewModelService( + _customerProductServiceMock.Object, + _productServiceMock.Object, + _storeServiceMock.Object, + _vendorServiceMock.Object, + translationServiceMock.Object, + _enumTranslationServiceMock.Object); + } + + [TestMethod] + public async Task PrepareCustomerModelAddProductModel_BuildsAvailableLists() + { + _storeServiceMock.Setup(s => s.GetAllStores()) + .ReturnsAsync(new List { new() { Id = "store-1", Shortcut = "Store 1" } }); + _vendorServiceMock + .Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "vendor-1", Name = "Vendor 1" } }); + _enumTranslationServiceMock + .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SelectList(Enumerable.Empty())); + + var model = await _customerProductViewModelService.PrepareCustomerModelAddProductModel(); + + Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store-1")); + Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor-1")); + Assert.IsTrue(model.AvailableProductTypes.Count > 0); + } + + [TestMethod] + public async Task UpdateProductPrice_UpdatesWhenFound() + { + var price = new CustomerProductPrice { Id = "pp1", Price = 1 }; + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + _customerProductServiceMock.Setup(c => c.UpdateCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.UpdateProductPrice( + new CustomerModel.ProductPriceModel { Id = "pp1", Price = 99 }); + + _customerProductServiceMock.Verify( + c => c.UpdateCustomerProductPrice(It.Is(x => x.Price == 99)), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_DeletesWhenFound() + { + var price = new CustomerProductPrice { Id = "pp1" }; + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + _customerProductServiceMock.Setup(c => c.DeleteCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.DeleteProductPrice("pp1"); + + _customerProductServiceMock.Verify(c => c.DeleteCustomerProductPrice(price), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_NotFound_Throws() + { + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById(It.IsAny())) + .ReturnsAsync((CustomerProductPrice)null); + + await Assert.ThrowsAsync( + () => _customerProductViewModelService.DeleteProductPrice("missing")); + } + + [TestMethod] + public async Task UpdatePersonalizedProduct_UpdatesWhenFound() + { + var customerProduct = new CustomerProduct { Id = "cp1", DisplayOrder = 1 }; + _customerProductServiceMock.Setup(c => c.GetCustomerProduct("cp1")).ReturnsAsync(customerProduct); + _customerProductServiceMock.Setup(c => c.UpdateCustomerProduct(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.UpdatePersonalizedProduct( + new CustomerModel.ProductModel { Id = "cp1", DisplayOrder = 7 }); + + _customerProductServiceMock.Verify( + c => c.UpdateCustomerProduct(It.Is(x => x.DisplayOrder == 7)), Times.Once); + } + + [TestMethod] + public async Task DeletePersonalizedProduct_NotFound_Throws() + { + _customerProductServiceMock.Setup(c => c.GetCustomerProduct(It.IsAny())) + .ReturnsAsync((CustomerProduct)null); + + await Assert.ThrowsAsync( + () => _customerProductViewModelService.DeletePersonalizedProduct("missing")); + } + + [TestMethod] + public async Task PrepareProductPriceModel_MapsItems() + { + _customerProductServiceMock + .Setup(c => c.GetProductsPriceByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "pp1", ProductId = "p1", Price = 3 } }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); + + var (items, _) = await _customerProductViewModelService.PrepareProductPriceModel("c1", 1, 10); + + var list = items.ToList(); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("Prod 1", list[0].ProductName); + Assert.AreEqual(3, list[0].Price); + } + + [TestMethod] + public async Task InsertCustomerAddProductModel_Personalized_InsertsCustomerProduct() + { + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Price = 5 }); + _customerProductServiceMock.Setup(c => c.GetCustomerProduct("c1", "p1")).ReturnsAsync((CustomerProduct)null); + _customerProductServiceMock.Setup(c => c.InsertCustomerProduct(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.InsertCustomerAddProductModel("c1", true, + new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); + + _customerProductServiceMock.Verify( + c => c.InsertCustomerProduct(It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1")), + Times.Once); + } + + [TestMethod] + public async Task InsertCustomerAddProductModel_NotPersonalized_InsertsProductPrice() + { + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Price = 5 }); + _customerProductServiceMock.Setup(c => c.GetPriceByCustomerProduct("c1", "p1")) + .ReturnsAsync((double?)null); + _customerProductServiceMock.Setup(c => c.InsertCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.InsertCustomerAddProductModel("c1", false, + new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); + + _customerProductServiceMock.Verify( + c => c.InsertCustomerProductPrice( + It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1" && x.Price == 5)), + Times.Once); + } + + [TestMethod] + public async Task PreparePersonalizedProducts_MapsItems() + { + _customerProductServiceMock + .Setup(c => c.GetProductsByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList + { new() { Id = "cp1", ProductId = "p1", DisplayOrder = 2 } }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); + + var (items, _) = await _customerProductViewModelService.PreparePersonalizedProducts("c1", 1, 10); + + var list = items.ToList(); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("Prod 1", list[0].ProductName); + Assert.AreEqual(2, list[0].DisplayOrder); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs new file mode 100644 index 000000000..aeefb15c0 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs @@ -0,0 +1,114 @@ +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerShoppingCartViewModelServiceTests +{ + private Mock _customerServiceMock; + private Mock _storeServiceMock; + private Mock _productServiceMock; + private Mock _shoppingCartServiceMock; + private CustomerShoppingCartViewModelService _service; + + [TestInitialize] + public void Setup() + { + _customerServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _productServiceMock = new Mock(); + _shoppingCartServiceMock = new Mock(); + + _service = new CustomerShoppingCartViewModelService( + _customerServiceMock.Object, + _storeServiceMock.Object, + _productServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _shoppingCartServiceMock.Object); + } + + [TestMethod] + public async Task PrepareShoppingCartItemModel_NoItemsOfTheRequestedType_ReturnsEmpty_WithoutLookups() + { + var customer = new Customer { Id = "c1" }; + customer.ShoppingCartItems.Add(new ShoppingCartItem { + Id = "w1", ShoppingCartTypeId = ShoppingCartType.Wishlist, ProductId = "p1" + }); + + var items = await _service.PrepareShoppingCartItemModel(customer, (int)ShoppingCartType.ShoppingCart); + + Assert.AreEqual(0, items.Count); + _productServiceMock.Verify(p => p.GetProductById(It.IsAny(), It.IsAny()), Times.Never); + _customerServiceMock.Verify(c => c.GetCustomerById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task PrepareShoppingCartItemModel_SkipsItemsWhoseProductNoLongerExists() + { + var customer = new Customer { Id = "c1" }; + customer.ShoppingCartItems.Add(new ShoppingCartItem { + Id = "sci1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, ProductId = "gone", StoreId = "s1" + }); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1" }); + _productServiceMock.Setup(p => p.GetProductById("gone", false)).ReturnsAsync((Product)null); + + var items = await _service.PrepareShoppingCartItemModel(customer, (int)ShoppingCartType.ShoppingCart); + + Assert.AreEqual(0, items.Count); + _productServiceMock.Verify(p => p.GetProductById("gone", false), Times.Once); + } + + [TestMethod] + public async Task DeleteCart_KnownItem_DeletesItAndSavesTheCustomer() + { + var customer = new Customer { Id = "c1" }; + var item = new ShoppingCartItem { Id = "sci1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart }; + customer.ShoppingCartItems.Add(item); + + await _service.DeleteCart(customer, "sci1"); + + _shoppingCartServiceMock.Verify(s => s.DeleteShoppingCartItem(customer, item, true, true), Times.Once); + _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); + } + + [TestMethod] + public async Task DeleteCart_UnknownItem_DoesNothing() + { + var customer = new Customer { Id = "c1" }; + + await _service.DeleteCart(customer, "missing"); + + _shoppingCartServiceMock.VerifyNoOtherCalls(); + _customerServiceMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task UpdateCart_UnknownItem_ReturnsNoWarnings_WithoutUpdating() + { + var customer = new Customer { Id = "c1" }; + + var warnings = await _service.UpdateCart(customer, "missing", 9.99); + + Assert.AreEqual(0, warnings.Count); + _shoppingCartServiceMock.VerifyNoOtherCalls(); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs index 3e710e180..17ce621e1 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs @@ -40,15 +40,9 @@ public class CustomerViewModelServiceTests private Mock _groupServiceMock; private Mock _storeServiceMock; private Mock _customerTagServiceMock; - private Mock _vendorServiceMock; private Mock _affiliateServiceMock; private Mock _enumTranslationServiceMock; - private Mock _customerNoteServiceMock; - private Mock _customerProductServiceMock; - private Mock _loyaltyPointsServiceMock; - private Mock _productServiceMock; private Mock _dateTimeServiceMock; - private Mock _downloadServiceMock; private Mock _newsLetterSubscriptionServiceMock; private Mock _translationServiceMock; private Customer _currentCustomer; @@ -61,15 +55,9 @@ public void Setup() _groupServiceMock = new Mock(); _storeServiceMock = new Mock(); _customerTagServiceMock = new Mock(); - _vendorServiceMock = new Mock(); _affiliateServiceMock = new Mock(); _enumTranslationServiceMock = new Mock(); - _customerNoteServiceMock = new Mock(); - _customerProductServiceMock = new Mock(); - _loyaltyPointsServiceMock = new Mock(); - _productServiceMock = new Mock(); _dateTimeServiceMock = new Mock(); - _downloadServiceMock = new Mock(); _newsLetterSubscriptionServiceMock = new Mock(); _translationServiceMock = new Mock(); _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns(k => k); @@ -123,39 +111,27 @@ public void Setup() It.IsAny())) .ReturnsAsync(new PagedList()); - _customerNoteServiceMock.Setup(c => c.InsertCustomerNote(It.IsAny())) - .Returns(Task.CompletedTask); - _groupServiceMock.Setup(g => g.GetAllByIds(It.IsAny())) .ReturnsAsync(new List()); _customerViewModelService = new CustomerViewModelService( _customerServiceMock.Object, _groupServiceMock.Object, - _customerProductServiceMock.Object, _newsLetterSubscriptionServiceMock.Object, _dateTimeServiceMock.Object, _translationServiceMock.Object, - _loyaltyPointsServiceMock.Object, new Mock().Object, contextAccessorMock.Object, - _vendorServiceMock.Object, _storeServiceMock.Object, new Mock().Object, customerAttributeServiceMock.Object, - new Mock().Object, - new Mock().Object, _affiliateServiceMock.Object, _customerTagServiceMock.Object, - _productServiceMock.Object, salesEmployeeServiceMock.Object, - _customerNoteServiceMock.Object, - _downloadServiceMock.Object, httpContextAccessorMock.Object, new CustomerSettings(), new TaxSettings(), new LoyaltyPointsSettings(), - new AddressSettings(), new CommonSettings(), _enumTranslationServiceMock.Object); } @@ -335,248 +311,6 @@ public async Task DeleteSelected_DeletesAllExceptCurrentUser() _customerServiceMock.Verify(c => c.DeleteCustomer(current), Times.Never); } - [TestMethod] - public async Task PrepareCustomerModelAddProductModel_BuildsAvailableLists() - { - _storeServiceMock.Setup(s => s.GetAllStores()) - .ReturnsAsync(new List { new() { Id = "store-1", Shortcut = "Store 1" } }); - _vendorServiceMock - .Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList { new() { Id = "vendor-1", Name = "Vendor 1" } }); - _enumTranslationServiceMock - .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new SelectList(Enumerable.Empty())); - - var model = await _customerViewModelService.PrepareCustomerModelAddProductModel(); - - Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store-1")); - Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor-1")); - Assert.IsTrue(model.AvailableProductTypes.Count > 0); - } - - [TestMethod] - public async Task InsertCustomerNote_InsertsNoteWithMappedFields() - { - var note = await _customerViewModelService.InsertCustomerNote("c1", "d1", false, "title", "message"); - - Assert.AreEqual("c1", note.CustomerId); - Assert.AreEqual("d1", note.DownloadId); - Assert.AreEqual("title", note.Title); - Assert.AreEqual("message", note.Note); - Assert.IsFalse(note.DisplayToCustomer); - _customerNoteServiceMock.Verify( - c => c.InsertCustomerNote(It.Is(x => x.CustomerId == "c1" && x.Title == "title")), - Times.Once); - } - - [TestMethod] - public async Task DeleteCustomerNote_DeletesNoteAndAttachment() - { - var note = new CustomerNote { Id = "n1", DownloadId = "d1" }; - var download = new Download { Id = "d1" }; - _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); - _customerNoteServiceMock.Setup(c => c.DeleteCustomerNote(It.IsAny())).Returns(Task.CompletedTask); - _downloadServiceMock.Setup(d => d.GetDownloadById("d1")).ReturnsAsync(download); - _downloadServiceMock.Setup(d => d.DeleteDownload(It.IsAny())).Returns(Task.CompletedTask); - - await _customerViewModelService.DeleteCustomerNote("n1", "c1"); - - _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(note), Times.Once); - _downloadServiceMock.Verify(d => d.DeleteDownload(download), Times.Once); - } - - [TestMethod] - public async Task DeleteCustomerNote_NotFound_Throws() - { - _customerNoteServiceMock.Setup(c => c.GetCustomerNote(It.IsAny())) - .ReturnsAsync((CustomerNote)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeleteCustomerNote("missing", "c1")); - } - - [TestMethod] - public async Task PrepareLoyaltyPointsHistoryModel_MapsHistory() - { - _loyaltyPointsServiceMock - .Setup(l => l.GetLoyaltyPointsHistory(It.IsAny(), It.IsAny(), true)) - .ReturnsAsync(new List { - new() { StoreId = "store-1", Points = 5, PointsBalance = 10, Message = "added" } - }); - _storeServiceMock.Setup(s => s.GetStoreById("store-1")) - .ReturnsAsync(new Grand.Domain.Stores.Store { Id = "store-1", Shortcut = "Store 1" }); - - var result = (await _customerViewModelService.PrepareLoyaltyPointsHistoryModel("c1")).ToList(); - - Assert.AreEqual(1, result.Count); - Assert.AreEqual("Store 1", result[0].StoreName); - Assert.AreEqual(5, result[0].Points); - } - - [TestMethod] - public async Task InsertLoyaltyPointsHistory_DelegatesToService() - { - var history = new LoyaltyPointsHistory(); - _loyaltyPointsServiceMock - .Setup(l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny())) - .ReturnsAsync(history); - - var result = await _customerViewModelService.InsertLoyaltyPointsHistory( - new Customer { Id = "c1" }, "store-1", 10, "msg"); - - Assert.AreSame(history, result); - _loyaltyPointsServiceMock.Verify( - l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny()), - Times.Once); - } - - [TestMethod] - public async Task DeleteAddress_RemovesAddressAndUpdatesCustomer() - { - var address = new Address { Id = "a1" }; - var customer = new Customer(); - customer.Addresses.Add(address); - - await _customerViewModelService.DeleteAddress(customer, address); - - Assert.IsFalse(customer.Addresses.Any(a => a.Id == "a1")); - _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); - } - - [TestMethod] - public async Task UpdateProductPrice_UpdatesWhenFound() - { - var price = new CustomerProductPrice { Id = "pp1", Price = 1 }; - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - _customerProductServiceMock.Setup(c => c.UpdateCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.UpdateProductPrice( - new CustomerModel.ProductPriceModel { Id = "pp1", Price = 99 }); - - _customerProductServiceMock.Verify( - c => c.UpdateCustomerProductPrice(It.Is(x => x.Price == 99)), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_DeletesWhenFound() - { - var price = new CustomerProductPrice { Id = "pp1" }; - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - _customerProductServiceMock.Setup(c => c.DeleteCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.DeleteProductPrice("pp1"); - - _customerProductServiceMock.Verify(c => c.DeleteCustomerProductPrice(price), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_NotFound_Throws() - { - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById(It.IsAny())) - .ReturnsAsync((CustomerProductPrice)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeleteProductPrice("missing")); - } - - [TestMethod] - public async Task UpdatePersonalizedProduct_UpdatesWhenFound() - { - var customerProduct = new CustomerProduct { Id = "cp1", DisplayOrder = 1 }; - _customerProductServiceMock.Setup(c => c.GetCustomerProduct("cp1")).ReturnsAsync(customerProduct); - _customerProductServiceMock.Setup(c => c.UpdateCustomerProduct(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.UpdatePersonalizedProduct( - new CustomerModel.ProductModel { Id = "cp1", DisplayOrder = 7 }); - - _customerProductServiceMock.Verify( - c => c.UpdateCustomerProduct(It.Is(x => x.DisplayOrder == 7)), Times.Once); - } - - [TestMethod] - public async Task DeletePersonalizedProduct_NotFound_Throws() - { - _customerProductServiceMock.Setup(c => c.GetCustomerProduct(It.IsAny())) - .ReturnsAsync((CustomerProduct)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeletePersonalizedProduct("missing")); - } - - [TestMethod] - public async Task PrepareProductPriceModel_MapsItems() - { - _customerProductServiceMock - .Setup(c => c.GetProductsPriceByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList { new() { Id = "pp1", ProductId = "p1", Price = 3 } }); - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); - - var (items, _) = await _customerViewModelService.PrepareProductPriceModel("c1", 1, 10); - - var list = items.ToList(); - Assert.AreEqual(1, list.Count); - Assert.AreEqual("Prod 1", list[0].ProductName); - Assert.AreEqual(3, list[0].Price); - } - - [TestMethod] - public async Task InsertCustomerAddProductModel_Personalized_InsertsCustomerProduct() - { - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Price = 5 }); - _customerProductServiceMock.Setup(c => c.GetCustomerProduct("c1", "p1")).ReturnsAsync((CustomerProduct)null); - _customerProductServiceMock.Setup(c => c.InsertCustomerProduct(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.InsertCustomerAddProductModel("c1", true, - new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); - - _customerProductServiceMock.Verify( - c => c.InsertCustomerProduct(It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1")), - Times.Once); - } - - [TestMethod] - public async Task InsertCustomerAddProductModel_NotPersonalized_InsertsProductPrice() - { - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Price = 5 }); - _customerProductServiceMock.Setup(c => c.GetPriceByCustomerProduct("c1", "p1")) - .ReturnsAsync((double?)null); - _customerProductServiceMock.Setup(c => c.InsertCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.InsertCustomerAddProductModel("c1", false, - new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); - - _customerProductServiceMock.Verify( - c => c.InsertCustomerProductPrice( - It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1" && x.Price == 5)), - Times.Once); - } - - [TestMethod] - public async Task PreparePersonalizedProducts_MapsItems() - { - _customerProductServiceMock - .Setup(c => c.GetProductsByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList - { new() { Id = "cp1", ProductId = "p1", DisplayOrder = 2 } }); - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); - - var (items, _) = await _customerViewModelService.PreparePersonalizedProducts("c1", 1, 10); - - var list = items.ToList(); - Assert.AreEqual(1, list.Count); - Assert.AreEqual("Prod 1", list[0].ProductName); - Assert.AreEqual(2, list[0].DisplayOrder); - } - [TestMethod] public async Task DeleteCustomer_RemovesNewsletterSubscriptions() { diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs index 94099932a..de0b08ff4 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs @@ -1,6 +1,7 @@ using System.Reflection; using Grand.Web.Common.Filters; using Grand.Web.Store.Controllers; +using Grand.Web.Store.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -67,16 +68,49 @@ public void CustomerController_DoesNotInheritBaseCustomerManagementController() } [TestMethod] - public void CustomerController_DeclaresOnlyItsThreeOwnMembers() + public void CustomerController_DeclaresOnlyItsStoreOnlyMembers() { - // The per-store gate override, PerStoreDisabled, and ApplyPostConstraints override are - // genuine Store-only infrastructure — everything else must live in BaseCustomerController. + // PerStoreDisabled and the ApplyPostConstraints override are genuine Store-only + // infrastructure — everything else must live in BaseCustomerController. var ownMethods = typeof(CustomerController).GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); var ownMethodNames = ownMethods.Select(m => m.Name) .Where(n => !n.StartsWith("get_") && !n.StartsWith("set_")).ToHashSet(); - var allowed = new HashSet { "OnActionExecutionAsync", "PerStoreDisabled", "ApplyPostConstraints", ".ctor" }; + var allowed = new HashSet { "PerStoreDisabled", "ApplyPostConstraints", ".ctor" }; Assert.IsTrue(ownMethodNames.IsSubsetOf(allowed), $"Unexpected own members on thin Store subclass: {string.Join(", ", ownMethodNames.Except(allowed))}"); } + + private static readonly Type[] CustomerControllers = [ + typeof(CustomerController), + typeof(CustomerAddressController), + typeof(CustomerLoyaltyPointsController), + typeof(CustomerShoppingCartController), + typeof(CustomerProductController), + typeof(CustomerOutOfStockSubscriptionController), + typeof(CustomerOrderController), + typeof(CustomerReviewController) + ]; + + [TestMethod] + public void CustomerControllers_AllCarryThePerStoreGate() + { + // The tab controllers serve the same customers as CustomerController, so turning + // Customer:RegisterCustomersPerStore off must close them too, not only the list and the form. + foreach (var type in CustomerControllers) + Assert.AreEqual(1, type.GetCustomAttributes(typeof(PerStoreCustomersAttribute), inherit: false).Length, + $"{type.Name} must carry [PerStoreCustomers]."); + } + + [TestMethod] + public void CustomerControllers_AllCarryStoreAreaAndAuthorization() + { + foreach (var type in CustomerControllers) + { + var area = type.GetCustomAttributes(typeof(AreaAttribute), inherit: false).Cast().Single(); + Assert.AreEqual("Store", area.RouteValue, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeStoreAttribute), inherit: false).Length, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false).Length, type.Name); + } + } } diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs index c6689bd14..9fe50e620 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs @@ -1,8 +1,5 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Utilities.Customers; using Grand.Domain.Customers; @@ -12,9 +9,9 @@ using Grand.Web.AdminShared.Models.Customers; using Grand.Web.Common.Models; using Grand.Web.Store.Controllers; +using Grand.Web.Store.Filters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -25,10 +22,9 @@ namespace Grand.Web.Store.Tests.Controllers; -// Trimmed for the Task 11 thin-subclass cutover: business logic now shared through -// BaseCustomerController is exercised by BaseCustomerControllerTests.cs (Admin.Tests, Tasks 4-8) -// and applies identically to Store. What remains here is genuinely Store-only: the per-store -// feature gate (OnActionExecutionAsync), the PerStoreDisabled page, and the +// Business logic shared through BaseCustomerController and the customer tab controllers is +// exercised in Admin.Tests and applies identically to Store. What remains here is genuinely +// Store-only: the per-store feature gate ([PerStoreCustomers]), the PerStoreDisabled page, and the // ApplyPostConstraints override's anti-smuggling contract. [TestClass] public class CustomerControllerTests @@ -71,52 +67,36 @@ private class TestableCustomerController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, ICustomerManagerService customerManagerService, - ICustomerProductService customerProductService, - IProductReviewService productReviewService, - IProductReviewViewModelService productReviewViewModelService, - IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, ICustomerAttributeService customerAttributeService, - IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IMessageProviderService messageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, - IAdminDataScope scope, - CustomerConfig customerConfig) + IAdminDataScope scope) : CustomerController(customerService, customerViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, productViewModelService, - customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, - customerConfig) + customerAttributeParser, customerAttributeService, messageProviderService, groupService, + translationService, contextAccessor, customerSettings, scope) { public Task ApplyPostConstraintsPublic(CustomerModel model) => ApplyPostConstraints(model); } - private TestableCustomerController BuildStoreController(bool perStoreEnabled = true, - IGroupService groupService = null, IAdminDataScope scope = null) + private TestableCustomerController BuildStoreController(IGroupService groupService = null, + IAdminDataScope scope = null) { var controller = new TestableCustomerController( _customerServiceMock.Object, _customerViewModelServiceMock.Object, _customerManagerServiceMock.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, _customerAttributeServiceMock.Object, - new Mock().Object, - new Mock().Object, new Mock().Object, groupService ?? _groupServiceMock.Object, _translationServiceMock.Object, _contextAccessorMock.Object, new CustomerSettings(), - scope ?? _scopeMock.Object, - new CustomerConfig { RegisterCustomersPerStore = perStoreEnabled }); + scope ?? _scopeMock.Object); var httpContext = new DefaultHttpContext(); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; @@ -124,45 +104,54 @@ private TestableCustomerController BuildStoreController(bool perStoreEnabled = t return controller; } - private static ActionExecutingContext BuildGate(CustomerController controller, string actionName) + private static async Task<(ActionExecutingContext context, bool nextCalled)> RunGate(bool perStoreEnabled, + string controllerName, string actionName) { - var actionContext = new ActionContext(controller.ControllerContext.HttpContext, new RouteData(), - new ControllerActionDescriptor { ActionName = actionName }); - return new ActionExecutingContext(actionContext, new List(), - new Dictionary(), controller); + var filter = new PerStoreCustomersAttribute.PerStoreCustomersFilter( + new CustomerConfig { RegisterCustomersPerStore = perStoreEnabled }); + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), + new ControllerActionDescriptor { ControllerName = controllerName, ActionName = actionName }); + var context = new ActionExecutingContext(actionContext, new List(), + new Dictionary(), null); + var nextCalled = false; + + await filter.OnActionExecutionAsync(context, () => + { + nextCalled = true; + return Task.FromResult(new ActionExecutedContext(context, new List(), null)); + }); + + return (context, nextCalled); } [TestMethod] public async Task Gate_PerStoreDisabled_RedirectsToPerStoreDisabled() { - var controller = BuildStoreController(perStoreEnabled: false); - var context = BuildGate(controller, nameof(CustomerController.List)); - var nextCalled = false; + var (context, nextCalled) = await RunGate(false, "Customer", nameof(CustomerController.List)); - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + Assert.IsFalse(nextCalled); + var redirect = context.Result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual(nameof(CustomerController.PerStoreDisabled), redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task Gate_PerStoreDisabled_TabControllerAction_RedirectsToCustomerPerStoreDisabled() + { + var (context, nextCalled) = await RunGate(false, "CustomerAddress", "AddressCreate"); Assert.IsFalse(nextCalled); var redirect = context.Result as RedirectToActionResult; Assert.IsNotNull(redirect); Assert.AreEqual(nameof(CustomerController.PerStoreDisabled), redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); } [TestMethod] public async Task Gate_PerStoreDisabled_AllowsPerStoreDisabledAction() { - var controller = BuildStoreController(perStoreEnabled: false); - var context = BuildGate(controller, nameof(CustomerController.PerStoreDisabled)); - var nextCalled = false; - - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + var (context, nextCalled) = await RunGate(false, "Customer", nameof(CustomerController.PerStoreDisabled)); Assert.IsTrue(nextCalled); Assert.IsNull(context.Result); @@ -171,15 +160,7 @@ await controller.OnActionExecutionAsync(context, () => [TestMethod] public async Task Gate_PerStoreEnabled_CallsNext() { - var controller = BuildStoreController(perStoreEnabled: true); - var context = BuildGate(controller, nameof(CustomerController.List)); - var nextCalled = false; - - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + var (context, nextCalled) = await RunGate(true, "Customer", nameof(CustomerController.List)); Assert.IsTrue(nextCalled); Assert.IsNull(context.Result); @@ -188,7 +169,7 @@ await controller.OnActionExecutionAsync(context, () => [TestMethod] public void PerStoreDisabled_ReturnsView() { - var controller = BuildStoreController(perStoreEnabled: false); + var controller = BuildStoreController(); var result = controller.PerStoreDisabled(); Assert.IsInstanceOfType(result, typeof(ViewResult)); } diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml index 69dc189e7..0ac2b1a2a 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml @@ -15,7 +15,7 @@
diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml index 6e03b0e78..ae32a1a1e 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml @@ -9,8 +9,8 @@
@@ -102,7 +102,7 @@ $.ajax({ cache:false, type: "POST", - url: "@(Url.Action("CustomerNoteAdd", "Customer", new { area = Constants.AreaAdmin }))", + url: "@(Url.Action("CustomerNoteAdd", "CustomerNote", new { area = Constants.AreaAdmin }))", data: postData, success: function (data) { var grid = GrandAdmin.grids.get("#customernotes-grid"); diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml similarity index 81% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml rename to src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml index e7efa6955..c267c4e6d 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml @@ -3,13 +3,13 @@ //page title ViewBag.Title = Loc["Admin.Customers.Customers.Addresses.AddNew"]; } -
+ @* one address form: Save as the one primary action, back to the customer it belongs to *@ +
@@ -24,7 +24,7 @@ - {{ $texts.edit }} + {{ $texts.edit }} diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml index 90d91147a..54e1c6aac 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml @@ -14,9 +14,9 @@ @*unit prices are edited in the cells and sent with Save changes, one request per changed item*@ @@ -56,7 +56,7 @@ var dataItem = grid.dataSource.get(id); var tokenInput = $('input[name=__RequestVerificationToken]'); $.ajax({ - url: "@Html.Raw(Url.Action("DeleteCart", "Customer", new { customerId = Model.Id, area = area }))", + url: "@Html.Raw(Url.Action("DeleteCart", "CustomerShoppingCart", new { customerId = Model.Id, area = area }))", type: "POST", data: { id: dataItem.Id, diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml index 440beb0fe..088193893 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml @@ -12,8 +12,8 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml index 72ee6ee75..1fe392442 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml @@ -9,7 +9,7 @@
@if (area == "Admin" && !adminAreaSettings.HideStoreColumn) @@ -67,7 +67,7 @@ $.ajax({ cache:false, type: "POST", - url: "@(Url.Action("LoyaltyPointsHistoryAdd", "Customer", new { area = area }))", + url: "@(Url.Action("LoyaltyPointsHistoryAdd", "CustomerLoyaltyPoints", new { area = area }))", data: postData, success: function (data) { var grid = GrandAdmin.grids.get("#customer-loyaltypoints-grid"); diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml index b3435f1fc..c3b74247a 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml @@ -6,7 +6,7 @@
@@ -34,7 +34,7 @@ } - + {{ ProductName }} diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml index e9d703c9c..408b23e05 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml @@ -6,7 +6,7 @@
@if (area == "Admin" && !adminAreaSettings.HideStoreColumn) { diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml index d5ac209c4..5164786a7 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml @@ -4,15 +4,15 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml index 4db071eec..ba0afda40 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml @@ -4,15 +4,15 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml index db791f043..426808f26 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml @@ -5,8 +5,8 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdateAddress.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/CustomerAddress/Partials/CreateOrUpdateAddress.cshtml similarity index 100% rename from src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdateAddress.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/CustomerAddress/Partials/CreateOrUpdateAddress.cshtml diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml similarity index 79% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml rename to src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml index 797180afa..57602413e 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml @@ -2,13 +2,13 @@ @{ ViewBag.Title = Loc["Admin.Customers.Customers.Addresses.AddNew"]; } - + @* one address form: Save as the one primary action, back to the customer it belongs to *@