Skip to content

Commit ff40e3a

Browse files
ARCH-001 Phase 27: consolidate SearchController's Category/Collection/Brand picker (#828)
1 parent 9ec8003 commit ff40e3a

8 files changed

Lines changed: 496 additions & 178 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using Grand.Business.Core.Interfaces.Catalog.Brands;
2+
using Grand.Business.Core.Interfaces.Catalog.Categories;
3+
using Grand.Business.Core.Interfaces.Catalog.Collections;
4+
using Grand.Domain;
5+
using Grand.Domain.Admin;
6+
using Grand.Domain.Catalog;
7+
using Grand.Web.AdminShared.Controllers;
8+
using Grand.Web.Common.DataSource;
9+
using Microsoft.AspNetCore.Mvc;
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using Moq;
12+
13+
namespace Grand.Web.Admin.Tests.Controllers;
14+
15+
[TestClass]
16+
public class BaseSearchControllerTests
17+
{
18+
// Concrete subclass exists only to instantiate the abstract base for direct-call unit tests,
19+
// with PickerStoreId parameterized so both the Admin/Vendor shape ("") and the Store shape (a
20+
// real store id) can be exercised without needing the 3 real host subclasses - same pattern as
21+
// BaseTaxCategoryControllerTests.
22+
private class TestSearchController(
23+
ICategoryService categoryService,
24+
IBrandService brandService,
25+
ICollectionService collectionService,
26+
AdminSearchSettings adminSearchSettings,
27+
string pickerStoreId)
28+
: BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings)
29+
{
30+
protected override string PickerStoreId => pickerStoreId;
31+
}
32+
33+
private Mock<ICategoryService> _categoryServiceMock = null!;
34+
private Mock<IBrandService> _brandServiceMock = null!;
35+
private Mock<ICollectionService> _collectionServiceMock = null!;
36+
private AdminSearchSettings _settings = null!;
37+
38+
[TestInitialize]
39+
public void Setup()
40+
{
41+
_categoryServiceMock = new Mock<ICategoryService>();
42+
_brandServiceMock = new Mock<IBrandService>();
43+
_collectionServiceMock = new Mock<ICollectionService>();
44+
_settings = new AdminSearchSettings {
45+
CategorySizeLimit = 10,
46+
CollectionSizeLimit = 10,
47+
BrandSizeLimit = 10
48+
};
49+
}
50+
51+
private TestSearchController CreateController(string pickerStoreId) =>
52+
new(_categoryServiceMock.Object, _brandServiceMock.Object, _collectionServiceMock.Object, _settings, pickerStoreId);
53+
54+
private static DataSourceRequestFilter EmptyFilter() => new() { Filters = [] };
55+
56+
// Anonymous JSON payloads aren't used here (DataSourceResult is a public type with public
57+
// members), but Data's items are instances of a private nested record - read them via
58+
// reflection, same pattern established in BasePictureControllerTests.
59+
private static (string Id, string Name)[] ReadRows(IActionResult result)
60+
{
61+
var data = (System.Collections.IEnumerable)((JsonResult)result).Value!.GetType().GetProperty("Data")!
62+
.GetValue(((JsonResult)result).Value)!;
63+
var rows = new List<(string, string)>();
64+
foreach (var item in data)
65+
{
66+
var id = (string)item.GetType().GetProperty("Id")!.GetValue(item)!;
67+
var name = (string)item.GetType().GetProperty("Name")!.GetValue(item)!;
68+
rows.Add((id, name));
69+
}
70+
return rows.ToArray();
71+
}
72+
73+
[TestMethod]
74+
public async Task Category_DefaultPickerStoreId_PassesEmptyStoreIdToService()
75+
{
76+
var controller = CreateController("");
77+
_categoryServiceMock
78+
.Setup(s => s.GetAllCategories(null, null, "", 0, 10, false))
79+
.ReturnsAsync(new PagedList<Category>([new Category { Id = "cat-1", Name = "Books" }], 0, 10));
80+
_categoryServiceMock.Setup(s => s.GetFormattedBreadCrumb(It.IsAny<Category>(), ">>", ""))
81+
.ReturnsAsync("Books");
82+
83+
var result = await controller.Category(null, EmptyFilter());
84+
85+
var rows = ReadRows(result);
86+
Assert.AreEqual(1, rows.Length);
87+
Assert.AreEqual("cat-1", rows[0].Id);
88+
_categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "", 0, 10, false), Times.Once);
89+
}
90+
91+
[TestMethod]
92+
public async Task Category_OverriddenPickerStoreId_PassesStoreIdToService()
93+
{
94+
var controller = CreateController("store-1");
95+
_categoryServiceMock
96+
.Setup(s => s.GetAllCategories(null, null, "store-1", 0, 10, false))
97+
.ReturnsAsync(new PagedList<Category>([], 0, 10));
98+
99+
await controller.Category(null, EmptyFilter());
100+
101+
_categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-1", 0, 10, false), Times.Once);
102+
}
103+
104+
[TestMethod]
105+
public async Task Collection_DefaultPickerStoreId_PassesEmptyStoreIdToService()
106+
{
107+
var controller = CreateController("");
108+
_collectionServiceMock
109+
.Setup(s => s.GetAllCollections(null, "", 0, 10, false))
110+
.ReturnsAsync(new PagedList<Collection>([new Collection { Id = "col-1", Name = "Summer" }], 0, 10));
111+
112+
var result = await controller.Collection(null, EmptyFilter());
113+
114+
var rows = ReadRows(result);
115+
Assert.AreEqual(1, rows.Length);
116+
Assert.AreEqual("Summer", rows[0].Name);
117+
_collectionServiceMock.Verify(s => s.GetAllCollections(null, "", 0, 10, false), Times.Once);
118+
}
119+
120+
[TestMethod]
121+
public async Task Collection_OverriddenPickerStoreId_PassesStoreIdToService()
122+
{
123+
var controller = CreateController("store-1");
124+
_collectionServiceMock
125+
.Setup(s => s.GetAllCollections(null, "store-1", 0, 10, false))
126+
.ReturnsAsync(new PagedList<Collection>([], 0, 10));
127+
128+
await controller.Collection(null, EmptyFilter());
129+
130+
_collectionServiceMock.Verify(s => s.GetAllCollections(null, "store-1", 0, 10, false), Times.Once);
131+
}
132+
133+
[TestMethod]
134+
public async Task Brand_DefaultPickerStoreId_PassesEmptyStoreIdToService()
135+
{
136+
var controller = CreateController("");
137+
_brandServiceMock
138+
.Setup(s => s.GetAllBrands(null, "", 0, 10, false))
139+
.ReturnsAsync(new PagedList<Brand>([new Brand { Id = "brand-1", Name = "Acme" }], 0, 10));
140+
141+
var result = await controller.Brand(null, EmptyFilter());
142+
143+
var rows = ReadRows(result);
144+
Assert.AreEqual(1, rows.Length);
145+
Assert.AreEqual("Acme", rows[0].Name);
146+
_brandServiceMock.Verify(s => s.GetAllBrands(null, "", 0, 10, false), Times.Once);
147+
}
148+
149+
[TestMethod]
150+
public async Task Brand_OverriddenPickerStoreId_PassesStoreIdToService()
151+
{
152+
var controller = CreateController("store-1");
153+
_brandServiceMock
154+
.Setup(s => s.GetAllBrands(null, "store-1", 0, 10, false))
155+
.ReturnsAsync(new PagedList<Brand>([], 0, 10));
156+
157+
await controller.Brand(null, EmptyFilter());
158+
159+
_brandServiceMock.Verify(s => s.GetAllBrands(null, "store-1", 0, 10, false), Times.Once);
160+
}
161+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Linq;
2+
using Grand.Web.Admin.Controllers;
3+
using Grand.Web.AdminShared.Controllers;
4+
using Grand.Web.Common.Filters;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Grand.Web.Admin.Tests.Controllers;
9+
10+
[TestClass]
11+
public class SearchControllerAttributeTests
12+
{
13+
[TestMethod]
14+
public void IsSubclassOfBaseSearchController()
15+
{
16+
Assert.IsTrue(typeof(BaseSearchController).IsAssignableFrom(typeof(SearchController)));
17+
Assert.AreEqual(typeof(BaseSearchController), typeof(SearchController).BaseType);
18+
}
19+
20+
[TestMethod]
21+
public void HasAuthorizeAdminAttribute()
22+
{
23+
var attr = typeof(SearchController).GetCustomAttributes(typeof(AuthorizeAdminAttribute), inherit: false);
24+
Assert.AreEqual(1, attr.Length);
25+
}
26+
27+
[TestMethod]
28+
public void HasAreaAdminAttribute()
29+
{
30+
var attr = typeof(SearchController)
31+
.GetCustomAttributes(typeof(AreaAttribute), inherit: false)
32+
.Cast<AreaAttribute>().Single();
33+
Assert.AreEqual("Admin", attr.RouteValue);
34+
}
35+
36+
[TestMethod]
37+
public void HasAutoValidateAntiforgeryTokenAttribute()
38+
{
39+
var attr = typeof(SearchController)
40+
.GetCustomAttributes(typeof(AutoValidateAntiforgeryTokenAttribute), inherit: true);
41+
Assert.AreEqual(1, attr.Length);
42+
}
43+
44+
[TestMethod]
45+
public void HasAuthorizeMenuAttribute()
46+
{
47+
var attr = typeof(SearchController).GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false);
48+
Assert.AreEqual(1, attr.Length);
49+
}
50+
51+
// Regression guard: Index/CustomerGroup/Stores/Vendor have no Store/Vendor equivalent and must
52+
// stay declared directly on Admin's own concrete controller, not migrate into the shared base
53+
// (which would incorrectly expose them to Store/Vendor's SearchController too).
54+
[TestMethod]
55+
public void DeclaresHostOnlyPickerMethods()
56+
{
57+
var declaredMethodNames = typeof(SearchController)
58+
.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly)
59+
.Select(m => m.Name)
60+
.ToHashSet();
61+
62+
Assert.IsTrue(declaredMethodNames.Contains("Index"));
63+
Assert.IsTrue(declaredMethodNames.Contains("CustomerGroup"));
64+
Assert.IsTrue(declaredMethodNames.Contains("Stores"));
65+
Assert.IsTrue(declaredMethodNames.Contains("Vendor"));
66+
}
67+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Linq;
2+
using Grand.Domain.Admin;
3+
using Grand.Domain.Customers;
4+
using Grand.Business.Core.Interfaces.Catalog.Brands;
5+
using Grand.Business.Core.Interfaces.Catalog.Categories;
6+
using Grand.Business.Core.Interfaces.Catalog.Collections;
7+
using Grand.Infrastructure;
8+
using Grand.Web.AdminShared.Controllers;
9+
using Grand.Web.Common.Filters;
10+
using Grand.Web.Store.Controllers;
11+
using Microsoft.AspNetCore.Mvc;
12+
using Microsoft.VisualStudio.TestTools.UnitTesting;
13+
using Moq;
14+
15+
namespace Grand.Web.Store.Tests.Controllers;
16+
17+
[TestClass]
18+
public class SearchControllerAttributeTests
19+
{
20+
[TestMethod]
21+
public void IsSubclassOfBaseSearchController()
22+
{
23+
Assert.IsTrue(typeof(BaseSearchController).IsAssignableFrom(typeof(SearchController)));
24+
Assert.AreEqual(typeof(BaseSearchController), typeof(SearchController).BaseType);
25+
}
26+
27+
[TestMethod]
28+
public void HasAuthorizeStoreAttribute()
29+
{
30+
var attr = typeof(SearchController).GetCustomAttributes(typeof(AuthorizeStoreAttribute), inherit: false);
31+
Assert.AreEqual(1, attr.Length);
32+
}
33+
34+
[TestMethod]
35+
public void HasAreaStoreAttribute()
36+
{
37+
var attr = typeof(SearchController)
38+
.GetCustomAttributes(typeof(AreaAttribute), inherit: false)
39+
.Cast<AreaAttribute>().Single();
40+
Assert.AreEqual("Store", attr.RouteValue);
41+
}
42+
43+
[TestMethod]
44+
public void HasAutoValidateAntiforgeryTokenAttribute()
45+
{
46+
var attr = typeof(SearchController)
47+
.GetCustomAttributes(typeof(AutoValidateAntiforgeryTokenAttribute), inherit: true);
48+
Assert.AreEqual(1, attr.Length);
49+
}
50+
51+
[TestMethod]
52+
public void HasAuthorizeMenuAttribute()
53+
{
54+
var attr = typeof(SearchController).GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false);
55+
Assert.AreEqual(1, attr.Length);
56+
}
57+
58+
// Store is the one host whose PickerStoreId override actually matters (Admin/Vendor keep the
59+
// base's "" default) - regression guard for the storeId source itself, not just wiring.
60+
[TestMethod]
61+
public async System.Threading.Tasks.Task PickerStoreId_ReflectsCurrentCustomerStaffStoreId()
62+
{
63+
var categoryServiceMock = new Mock<ICategoryService>();
64+
categoryServiceMock
65+
.Setup(s => s.GetAllCategories(null, null, "store-42", 0, It.IsAny<int>(), false))
66+
.ReturnsAsync(new Grand.Domain.PagedList<Grand.Domain.Catalog.Category>([], 0, 10));
67+
68+
var workContextMock = new Mock<IWorkContext>();
69+
workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = "store-42" });
70+
var contextAccessorMock = new Mock<IContextAccessor>();
71+
contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object);
72+
73+
var controller = new SearchController(
74+
categoryServiceMock.Object,
75+
new Mock<IBrandService>().Object,
76+
new Mock<ICollectionService>().Object,
77+
new AdminSearchSettings { CategorySizeLimit = 10 },
78+
contextAccessorMock.Object);
79+
80+
await controller.Category(null, new Grand.Web.Common.DataSource.DataSourceRequestFilter { Filters = [] });
81+
82+
categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-42", 0, 10, false), Times.Once);
83+
}
84+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Grand.Web.AdminShared.Controllers;
2+
using Grand.Web.Common.Filters;
3+
using Grand.Web.Vendor.Controllers;
4+
using Grand.Web.Vendor.Extensions;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Grand.Web.Vendor.Tests.Controllers;
9+
10+
[TestClass]
11+
public class SearchControllerSurfaceTests
12+
{
13+
[TestMethod]
14+
public void VendorSearchController_IsSubclassOfBaseSearchController()
15+
{
16+
Assert.IsTrue(typeof(BaseSearchController).IsAssignableFrom(typeof(SearchController)));
17+
Assert.AreEqual(typeof(BaseSearchController), typeof(SearchController).BaseType);
18+
}
19+
20+
// Same defect class Task 17 (Order) and Task-level reviews on later phases caught: BaseSearchController
21+
// can't carry a host's [Area]/[Authorize*] attributes itself (they differ per host), so each concrete
22+
// subclass must restate its own - a missing one here would 404 or deauthorize the whole controller
23+
// silently. Same shape as VendorReviewControllerSurfaceTests/OrderControllerSurfaceTests.
24+
[TestMethod]
25+
public void VendorSearchController_HasAreaAttributeWithVendorArea()
26+
{
27+
var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(typeof(SearchController), typeof(AreaAttribute), false);
28+
Assert.IsNotNull(areaAttr, "Missing [Area].");
29+
Assert.AreEqual(Constants.AreaVendor, areaAttr.RouteValue);
30+
}
31+
32+
[TestMethod]
33+
public void VendorSearchController_HasAuthorizeVendorAttribute() =>
34+
Assert.IsTrue(typeof(SearchController).IsDefined(typeof(AuthorizeVendorAttribute), false),
35+
"Missing [AuthorizeVendor].");
36+
37+
[TestMethod]
38+
public void VendorSearchController_HasAutoValidateAntiforgeryTokenAttribute() =>
39+
Assert.IsTrue(typeof(SearchController).IsDefined(typeof(AutoValidateAntiforgeryTokenAttribute), true),
40+
"Missing [AutoValidateAntiforgeryToken].");
41+
42+
[TestMethod]
43+
public void VendorSearchController_HasAuthorizeMenuAttribute() =>
44+
Assert.IsTrue(typeof(SearchController).IsDefined(typeof(AuthorizeMenuAttribute), false),
45+
"Missing [AuthorizeMenu].");
46+
}

0 commit comments

Comments
 (0)