From 184e919f447f03ab4541e7dbcb91ea1b5ae60fe1 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 23:04:49 +0200 Subject: [PATCH 1/2] Cache home page and menu brands and collections per customer groups The home page brand and collection blocks and the top menu's brand and collection lists were cached per store and language only, although the lists are filtered by the customer's groups. The first visitor's list was served to everyone: a group-limited collection showed up for guests and its link returned 404, and in the other order a group member lost it. Add the customer's group ids to the four keys, as the category home page and menu keys already do. The keys keep their Grand.brand. and Grand.collection. prefixes, so entity invalidation still clears them. --- .../Catalog/GetHomepageBrandsHandlerTests.cs | 78 ++++++++++++++ .../GetHomepageCollectionsHandlerTests.cs | 78 ++++++++++++++ .../Handlers/Catalog/GetMenuHandlerTests.cs | 101 ++++++++++++++++++ .../Grand.Web/Events/Cache/CacheKeyConst.cs | 12 ++- .../Catalog/GetHomepageBrandsHandler.cs | 4 +- .../Catalog/GetHomepageCollectionsHandler.cs | 5 +- .../Handlers/Catalog/GetMenuHandler.cs | 4 +- 7 files changed, 273 insertions(+), 9 deletions(-) create mode 100644 src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs create mode 100644 src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs create mode 100644 src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs new file mode 100644 index 0000000000..0b3f433753 --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs @@ -0,0 +1,78 @@ +using Grand.Business.Core.Interfaces.Catalog.Brands; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Media; +using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Grand.Web.Features.Handlers.Catalog; +using Grand.Web.Features.Models.Catalog; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Tests.Features.Handlers.Catalog; + +[TestClass] +public class GetHomepageBrandsHandlerTests +{ + private GetHomepageBrandsHandler _handler; + + [TestInitialize] + public void Init() + { + var brandServiceMock = new Mock(); + brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { + var brands = new List { new() { Id = "public", ShowOnHomePage = true } }; + if (access.Customer.Groups.Contains("vip")) + brands.Add(new Brand { Id = "vip-only", ShowOnHomePage = true }); + return new PagedList(brands, 0, int.MaxValue); + }); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(s => s.GetResource(It.IsAny())).Returns("{0}"); + + var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, + new CacheConfig { DefaultCacheTimeMinutes = 1 }); + + _handler = new GetHomepageBrandsHandler(cacheBase, brandServiceMock.Object, + new Mock().Object, translationServiceMock.Object, new MediaSettings()); + } + + private Task> Handle(params string[] groups) + { + var customer = new Customer(); + foreach (var group in groups) customer.Groups.Add(group); + return _handler.Handle(new GetHomepageBrands { + Customer = customer, + Store = new Domain.Stores.Store { Id = "store-1" }, + Language = new Language { Id = "lang-1" } + }, CancellationToken.None); + } + + [TestMethod] + public async Task Handle_GuestAfterGroupMember_DoesNotSeeGroupLimitedBrand() + { + var vip = await Handle("registered", "vip"); + var guest = await Handle("guests"); + + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); + CollectionAssert.AreEquivalent(new[] { "public" }, guest.Select(x => x.Id).ToArray()); + } + + [TestMethod] + public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedBrand() + { + await Handle("guests"); + var vip = await Handle("registered", "vip"); + + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); + } +} diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs new file mode 100644 index 0000000000..42b69f87bd --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs @@ -0,0 +1,78 @@ +using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Media; +using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Grand.Web.Features.Handlers.Catalog; +using Grand.Web.Features.Models.Catalog; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Tests.Features.Handlers.Catalog; + +[TestClass] +public class GetHomepageCollectionsHandlerTests +{ + private GetHomepageCollectionsHandler _handler; + + [TestInitialize] + public void Init() + { + var collectionServiceMock = new Mock(); + collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { + var collections = new List { new() { Id = "public", ShowOnHomePage = true } }; + if (access.Customer.Groups.Contains("vip")) + collections.Add(new Collection { Id = "vip-only", ShowOnHomePage = true }); + return new PagedList(collections, 0, int.MaxValue); + }); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(s => s.GetResource(It.IsAny())).Returns("{0}"); + + var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, + new CacheConfig { DefaultCacheTimeMinutes = 1 }); + + _handler = new GetHomepageCollectionsHandler(cacheBase, collectionServiceMock.Object, + new Mock().Object, translationServiceMock.Object, new MediaSettings()); + } + + private Task> Handle(params string[] groups) + { + var customer = new Customer(); + foreach (var group in groups) customer.Groups.Add(group); + return _handler.Handle(new GetHomepageCollections { + Customer = customer, + Store = new Domain.Stores.Store { Id = "store-1" }, + Language = new Language { Id = "lang-1" } + }, CancellationToken.None); + } + + [TestMethod] + public async Task Handle_GuestAfterGroupMember_DoesNotSeeGroupLimitedCollection() + { + var vip = await Handle("registered", "vip"); + var guest = await Handle("guests"); + + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); + CollectionAssert.AreEquivalent(new[] { "public" }, guest.Select(x => x.Id).ToArray()); + } + + [TestMethod] + public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedCollection() + { + await Handle("guests"); + var vip = await Handle("registered", "vip"); + + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); + } +} diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs new file mode 100644 index 0000000000..82c503a56e --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs @@ -0,0 +1,101 @@ +using Grand.Business.Core.Interfaces.Catalog.Brands; +using Grand.Business.Core.Interfaces.Catalog.Categories; +using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain; +using Grand.Domain.Blogs; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Media; +using Grand.Domain.Pages; +using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Grand.Web.Features.Handlers.Catalog; +using Grand.Web.Features.Models.Catalog; +using Grand.Web.Models.Catalog; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Tests.Features.Handlers.Catalog; + +[TestClass] +public class GetMenuHandlerTests +{ + private GetMenuHandler _handler; + + [TestInitialize] + public void Init() + { + var brandServiceMock = new Mock(); + brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { + var brands = new List { new() { Id = "public", IncludeInMenu = true } }; + if (access.Customer.Groups.Contains("vip")) + brands.Add(new Brand { Id = "vip-only", IncludeInMenu = true }); + return new PagedList(brands, 0, int.MaxValue); + }); + + var collectionServiceMock = new Mock(); + collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { + var collections = new List { new() { Id = "public", IncludeInMenu = true } }; + if (access.Customer.Groups.Contains("vip")) + collections.Add(new Collection { Id = "vip-only", IncludeInMenu = true }); + return new PagedList(collections, 0, int.MaxValue); + }); + + var categoryServiceMock = new Mock(); + categoryServiceMock.Setup(s => s.GetMenuCategories(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + var pageServiceMock = new Mock(); + pageServiceMock.Setup(s => s.GetAllPages(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, + new CacheConfig { DefaultCacheTimeMinutes = 1 }); + + _handler = new GetMenuHandler(categoryServiceMock.Object, brandServiceMock.Object, pageServiceMock.Object, + cacheBase, collectionServiceMock.Object, new Mock().Object, new CatalogSettings(), + new BlogSettings(), new MenuItemSettings(), new MediaSettings()); + } + + private Task Handle(params string[] groups) + { + var customer = new Customer(); + foreach (var group in groups) customer.Groups.Add(group); + return _handler.Handle(new GetMenu { + Customer = customer, + Store = new Domain.Stores.Store { Id = "store-1" }, + Language = new Language { Id = "lang-1" } + }, CancellationToken.None); + } + + [TestMethod] + public async Task Handle_GuestAfterGroupMember_DoesNotSeeGroupLimitedBrandsAndCollections() + { + await Handle("registered", "vip"); + var guest = await Handle("guests"); + + CollectionAssert.AreEquivalent(new[] { "public" }, guest.Brands.Select(x => x.Id).ToArray()); + CollectionAssert.AreEquivalent(new[] { "public" }, guest.Collections.Select(x => x.Id).ToArray()); + } + + [TestMethod] + public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedBrandsAndCollections() + { + await Handle("guests"); + var vip = await Handle("registered", "vip"); + + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Brands.Select(x => x.Id).ToArray()); + CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Collections.Select(x => x.Id).ToArray()); + } +} diff --git a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs index bd4a079f12..222cb9ea21 100644 --- a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs +++ b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs @@ -18,8 +18,9 @@ public static class CacheKeyConst /// /// {0} : current store ID /// {1} : language ID + /// {2} : groups of the current user /// - public const string BRAND_HOMEPAGE_KEY = "Grand.brand.navigation.homepage-{0}-{1}.pres"; + public const string BRAND_HOMEPAGE_KEY = "Grand.brand.navigation.homepage-{0}-{1}-{2}.pres"; /// /// Key for CollectionNavigationModel caching @@ -27,8 +28,9 @@ public static class CacheKeyConst /// /// {0} : language id /// {1} : current store ID + /// {2} : groups of the current user /// - public const string BRAND_NAVIGATION_MENU = "Grand.brand.navigation.menu-{0}-{1}.pres"; + public const string BRAND_NAVIGATION_MENU = "Grand.brand.navigation.menu-{0}-{1}-{2}.pres"; /// /// Key for CollectionNavigationModel caching @@ -47,8 +49,9 @@ public static class CacheKeyConst /// /// {0} : language id /// {1} : current store ID + /// {2} : groups of the current user /// - public const string COLLECTION_NAVIGATION_MENU = "Grand.collection.navigation.menu-{0}-{1}.pres"; + public const string COLLECTION_NAVIGATION_MENU = "Grand.collection.navigation.menu-{0}-{1}-{2}.pres"; /// /// Key for caching of collection displayed on home page @@ -56,8 +59,9 @@ public static class CacheKeyConst /// /// {0} : current store ID /// {1} : language ID + /// {2} : groups of the current user /// - public const string COLLECTION_HOMEPAGE_KEY = "Grand.collection.navigation.homepage-{0}-{1}.pres"; + public const string COLLECTION_HOMEPAGE_KEY = "Grand.collection.navigation.homepage-{0}-{1}-{2}.pres"; /// diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs index 36f1c0590e..2220fb8f3d 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; @@ -37,7 +38,8 @@ public GetHomepageBrandsHandler( public async Task> Handle(GetHomepageBrands request, CancellationToken cancellationToken) { - var brandsCacheKey = string.Format(CacheKeyConst.BRAND_HOMEPAGE_KEY, request.Store.Id, request.Language.Id); + var brandsCacheKey = string.Format(CacheKeyConst.BRAND_HOMEPAGE_KEY, request.Store.Id, request.Language.Id, + string.Join(",", request.Customer.GetCustomerGroupIds())); var model = await _cacheBase.GetAsync(brandsCacheKey, async () => { diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs index 6c78822489..01a619acc6 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; @@ -38,8 +39,8 @@ public GetHomepageCollectionsHandler( public async Task> Handle(GetHomepageCollections request, CancellationToken cancellationToken) { - var collectionsCacheKey = - string.Format(CacheKeyConst.COLLECTION_HOMEPAGE_KEY, request.Store.Id, request.Language.Id); + var collectionsCacheKey = string.Format(CacheKeyConst.COLLECTION_HOMEPAGE_KEY, request.Store.Id, + request.Language.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); var model = await _cacheBase.GetAsync(collectionsCacheKey, async () => { diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index 09bb4ac65f..b4ead81ab7 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -78,7 +78,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat }).ToList(); var brandCacheKey = string.Format(CacheKeyConst.BRAND_NAVIGATION_MENU, - request.Language.Id, request.Store.Id); + request.Language.Id, request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); var cachedBrandModel = await _cacheBase.GetAsync(brandCacheKey, async () => (await _brandService.GetAllBrands(brandName: "", @@ -94,7 +94,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat ); var collectionCacheKey = string.Format(CacheKeyConst.COLLECTION_NAVIGATION_MENU, - request.Language.Id, request.Store.Id); + request.Language.Id, request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); var cachedCollectionModel = await _cacheBase.GetAsync(collectionCacheKey, async () => (await _collectionService.GetAllCollections(collectionName: "", From c16e97e1ed363fc0158885c3202a1351c31fd3bb Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 06:31:00 +0200 Subject: [PATCH 2/2] Build the menu groups key once and test store and language key parts GetMenuHandler joined the customer's group ids three times per request; compute them once and reuse for the category, brand and collection keys. Cover the store and language parts of the home page and menu keys: a different store or language must not reuse another one's cached entry. --- .../Catalog/GetHomepageBrandsHandlerTests.cs | 28 +++++++++++--- .../GetHomepageCollectionsHandlerTests.cs | 28 +++++++++++--- .../Handlers/Catalog/GetMenuHandlerTests.cs | 37 +++++++++++++++---- .../Handlers/Catalog/GetMenuHandler.cs | 7 ++-- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs index 0b3f433753..7714288a9b 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageBrandsHandlerTests.cs @@ -22,12 +22,13 @@ namespace Grand.Web.Tests.Features.Handlers.Catalog; public class GetHomepageBrandsHandlerTests { private GetHomepageBrandsHandler _handler; + private Mock _brandServiceMock; [TestInitialize] public void Init() { - var brandServiceMock = new Mock(); - brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + _brandServiceMock = new Mock(); + _brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { var brands = new List { new() { Id = "public", ShowOnHomePage = true } }; @@ -42,18 +43,23 @@ public void Init() var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _handler = new GetHomepageBrandsHandler(cacheBase, brandServiceMock.Object, + _handler = new GetHomepageBrandsHandler(cacheBase, _brandServiceMock.Object, new Mock().Object, translationServiceMock.Object, new MediaSettings()); } private Task> Handle(params string[] groups) + { + return HandleIn("store-1", "lang-1", groups); + } + + private Task> HandleIn(string storeId, string languageId, params string[] groups) { var customer = new Customer(); foreach (var group in groups) customer.Groups.Add(group); return _handler.Handle(new GetHomepageBrands { Customer = customer, - Store = new Domain.Stores.Store { Id = "store-1" }, - Language = new Language { Id = "lang-1" } + Store = new Domain.Stores.Store { Id = storeId }, + Language = new Language { Id = languageId } }, CancellationToken.None); } @@ -75,4 +81,16 @@ public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedBrand() CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); } + + [TestMethod] + public async Task Handle_OtherStoreOrLanguage_DoesNotReuseCachedEntry() + { + await HandleIn("store-1", "lang-1", "guests"); + await HandleIn("store-2", "lang-1", "guests"); + await HandleIn("store-1", "lang-2", "guests"); + await HandleIn("store-1", "lang-1", "guests"); + + _brandServiceMock.Verify(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(3)); + } } diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs index 42b69f87bd..bf3ad4cfd1 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetHomepageCollectionsHandlerTests.cs @@ -22,12 +22,13 @@ namespace Grand.Web.Tests.Features.Handlers.Catalog; public class GetHomepageCollectionsHandlerTests { private GetHomepageCollectionsHandler _handler; + private Mock _collectionServiceMock; [TestInitialize] public void Init() { - var collectionServiceMock = new Mock(); - collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + _collectionServiceMock = new Mock(); + _collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { var collections = new List { new() { Id = "public", ShowOnHomePage = true } }; @@ -42,18 +43,23 @@ public void Init() var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _handler = new GetHomepageCollectionsHandler(cacheBase, collectionServiceMock.Object, + _handler = new GetHomepageCollectionsHandler(cacheBase, _collectionServiceMock.Object, new Mock().Object, translationServiceMock.Object, new MediaSettings()); } private Task> Handle(params string[] groups) + { + return HandleIn("store-1", "lang-1", groups); + } + + private Task> HandleIn(string storeId, string languageId, params string[] groups) { var customer = new Customer(); foreach (var group in groups) customer.Groups.Add(group); return _handler.Handle(new GetHomepageCollections { Customer = customer, - Store = new Domain.Stores.Store { Id = "store-1" }, - Language = new Language { Id = "lang-1" } + Store = new Domain.Stores.Store { Id = storeId }, + Language = new Language { Id = languageId } }, CancellationToken.None); } @@ -75,4 +81,16 @@ public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedCollection() CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Select(x => x.Id).ToArray()); } + + [TestMethod] + public async Task Handle_OtherStoreOrLanguage_DoesNotReuseCachedEntry() + { + await HandleIn("store-1", "lang-1", "guests"); + await HandleIn("store-2", "lang-1", "guests"); + await HandleIn("store-1", "lang-2", "guests"); + await HandleIn("store-1", "lang-1", "guests"); + + _collectionServiceMock.Verify(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(3)); + } } diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs index 82c503a56e..a141a9b587 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs @@ -28,12 +28,14 @@ namespace Grand.Web.Tests.Features.Handlers.Catalog; public class GetMenuHandlerTests { private GetMenuHandler _handler; + private Mock _brandServiceMock; + private Mock _collectionServiceMock; [TestInitialize] public void Init() { - var brandServiceMock = new Mock(); - brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + _brandServiceMock = new Mock(); + _brandServiceMock.Setup(s => s.GetAllBrands(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { var brands = new List { new() { Id = "public", IncludeInMenu = true } }; @@ -42,8 +44,8 @@ public void Init() return new PagedList(brands, 0, int.MaxValue); }); - var collectionServiceMock = new Mock(); - collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + _collectionServiceMock = new Mock(); + _collectionServiceMock.Setup(s => s.GetAllCollections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string _, CatalogAccess access, int _, int _) => { var collections = new List { new() { Id = "public", IncludeInMenu = true } }; @@ -63,19 +65,24 @@ public void Init() var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _handler = new GetMenuHandler(categoryServiceMock.Object, brandServiceMock.Object, pageServiceMock.Object, - cacheBase, collectionServiceMock.Object, new Mock().Object, new CatalogSettings(), + _handler = new GetMenuHandler(categoryServiceMock.Object, _brandServiceMock.Object, pageServiceMock.Object, + cacheBase, _collectionServiceMock.Object, new Mock().Object, new CatalogSettings(), new BlogSettings(), new MenuItemSettings(), new MediaSettings()); } private Task Handle(params string[] groups) + { + return HandleIn("store-1", "lang-1", groups); + } + + private Task HandleIn(string storeId, string languageId, params string[] groups) { var customer = new Customer(); foreach (var group in groups) customer.Groups.Add(group); return _handler.Handle(new GetMenu { Customer = customer, - Store = new Domain.Stores.Store { Id = "store-1" }, - Language = new Language { Id = "lang-1" } + Store = new Domain.Stores.Store { Id = storeId }, + Language = new Language { Id = languageId } }, CancellationToken.None); } @@ -98,4 +105,18 @@ public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedBrandsAndCollecti CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Brands.Select(x => x.Id).ToArray()); CollectionAssert.AreEquivalent(new[] { "public", "vip-only" }, vip.Collections.Select(x => x.Id).ToArray()); } + + [TestMethod] + public async Task Handle_OtherStoreOrLanguage_DoesNotReuseCachedEntry() + { + await HandleIn("store-1", "lang-1", "guests"); + await HandleIn("store-2", "lang-1", "guests"); + await HandleIn("store-1", "lang-2", "guests"); + await HandleIn("store-1", "lang-1", "guests"); + + _brandServiceMock.Verify(s => s.GetAllBrands(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(3)); + _collectionServiceMock.Verify(s => s.GetAllCollections(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(3)); + } } diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index b4ead81ab7..b3e07505e1 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -58,9 +58,10 @@ public GetMenuHandler( public async Task Handle(GetMenu request, CancellationToken cancellationToken) { + var customerGroups = string.Join(",", request.Customer.GetCustomerGroupIds()); var cacheKey = string.Format(CacheKeyConst.CATEGORIES_BY_MENU, request.Language.Id, - request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); + request.Store.Id, customerGroups); var cachedCategoriesModel = await _cacheBase.GetAsync(cacheKey, async () => await PrepareCategorySimpleModels( request.Language, await _categoryService.GetMenuCategories(customer: request.Customer, storeId: request.Store.Id))); @@ -78,7 +79,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat }).ToList(); var brandCacheKey = string.Format(CacheKeyConst.BRAND_NAVIGATION_MENU, - request.Language.Id, request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); + request.Language.Id, request.Store.Id, customerGroups); var cachedBrandModel = await _cacheBase.GetAsync(brandCacheKey, async () => (await _brandService.GetAllBrands(brandName: "", @@ -94,7 +95,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat ); var collectionCacheKey = string.Format(CacheKeyConst.COLLECTION_NAVIGATION_MENU, - request.Language.Id, request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); + request.Language.Id, request.Store.Id, customerGroups); var cachedCollectionModel = await _cacheBase.GetAsync(collectionCacheKey, async () => (await _collectionService.GetAllCollections(collectionName: "",