Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6161ae1
feat(arch001): add StoreMerchandiseReturnDataScope
KrzysztofPajak Aug 26, 2026
4e7620e
fix(arch001): remove redundant CanView override on StoreMerchandiseRe…
KrzysztofPajak Aug 26, 2026
50ba051
feat(arch001): add VendorMerchandiseReturnDataScope
KrzysztofPajak Aug 26, 2026
52437be
feat(arch001): add RoutedMerchandiseReturnDataScope and register IAdm…
KrzysztofPajak Aug 26, 2026
3b89d29
feat(arch001): rename PrepareReturnReqestListModel, unify SearchMerch…
KrzysztofPajak Aug 26, 2026
0b499fa
feat(arch001): add BaseMerchandiseReturnController skeleton with List…
KrzysztofPajak Aug 26, 2026
29fa445
fix(arch001): finish Task 4's PrepareReturnRequestListModel rename in…
KrzysztofPajak Aug 26, 2026
7bec783
feat(arch001): add GoToId/ProductsForMerchandiseReturn region to Base…
KrzysztofPajak Aug 26, 2026
6eaad3c
feat(arch001): add Edit (GET+POST) region to BaseMerchandiseReturnCon…
KrzysztofPajak Aug 26, 2026
ad6c4e3
feat(arch001): add Delete region to BaseMerchandiseReturnController
KrzysztofPajak Aug 26, 2026
d20dccb
feat(arch001): add merchandise return notes region with preserved Adm…
KrzysztofPajak Aug 26, 2026
0d00476
feat(arch001): cut Admin/Store/Vendor MerchandiseReturnController ove…
KrzysztofPajak Aug 26, 2026
0c0396a
feat(arch001): migrate Admin+Store MerchandiseReturn views into Grand…
KrzysztofPajak Aug 26, 2026
e53e12e
test(arch001): add cross-scope-type coverage, HasAccessToMerchandiseR…
KrzysztofPajak Aug 26, 2026
b47b9fe
test(arch001): add per-host MerchandiseReturn controller routing/attr…
KrzysztofPajak Aug 26, 2026
b412219
fix(arch001): restore Area/Authorize/AuthorizeMenu attributes on Admi…
KrzysztofPajak Aug 27, 2026
12a4860
fix(arch001): restore order-not-found guard in MerchandiseReturnNoteA…
KrzysztofPajak Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
extern alias StoreHost;
extern alias VendorHost;

using Grand.Domain.Permissions;
using Grand.Web.Common.Filters;
using Grand.Web.Common.Security.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Web.Admin.Tests.Controllers;

[TestClass]
public class MerchandiseReturnControllerAttributeTests
{
[TestMethod]
public void AdminController_DerivesFromBaseMerchandiseReturnController()
{
Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController)
.IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController)));
}

[TestMethod]
public void StoreController_DerivesFromBaseMerchandiseReturnController()
{
Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController)
.IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController)));
}

[TestMethod]
public void VendorController_DerivesFromBaseMerchandiseReturnController()
{
Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController)
.IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController)));
}

[TestMethod]
public void BaseController_HasPermissionAuthorizeAttribute_ForMerchandiseReturnsSystemName()
{
var attr = typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController)
.GetCustomAttributes(typeof(PermissionAuthorizeAttribute), inherit: false)
.Cast<PermissionAuthorizeAttribute>()
.SingleOrDefault();
Assert.IsNotNull(attr);
Assert.AreEqual(PermissionSystemName.MerchandiseReturns, attr.Permission);
}

[TestMethod]
public void BaseController_HasAutoValidateAntiforgeryTokenAttribute()
{
var attrs = typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController)
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute), inherit: false);
Assert.AreEqual(1, attrs.Length);
}

// Regression guard for the defect class caught live: BaseMerchandiseReturnController can't carry a
// host's [Area]/[Authorize*] attributes itself (they differ per host), so each concrete subclass
// must restate its own - a missing one here would 404 or deauthorize the whole controller silently
// (confirmed live: /Store/MerchandiseReturn/List 404'd before this fix). Same shape as
// OrderControllerRoutingTests / PaymentTransactionControllerRoutingTests.
[TestMethod]
public void AdminController_HasAreaAttributeWithAdminArea()
{
var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(
typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false);
Assert.IsNotNull(areaAttr, "Missing [Area].");
Assert.AreEqual(Grand.Web.Admin.Extensions.Constants.AreaAdmin, areaAttr.RouteValue);
}

[TestMethod]
public void AdminController_HasAuthorizeAdminAttribute() =>
Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeAdminAttribute), false),
"Missing [AuthorizeAdmin].");

[TestMethod]
public void AdminController_HasAuthorizeMenuAttribute() =>
Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false),
"Missing [AuthorizeMenu].");

[TestMethod]
public void StoreController_HasAreaAttributeWithStoreArea()
{
var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(
typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false);
Assert.IsNotNull(areaAttr, "Missing [Area].");
Assert.AreEqual(StoreHost::Grand.Web.Store.Extensions.Constants.AreaStore, areaAttr.RouteValue);
}

[TestMethod]
public void StoreController_HasAuthorizeStoreAttribute() =>
Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeStoreAttribute), false),
"Missing [AuthorizeStore].");

[TestMethod]
public void StoreController_HasAuthorizeMenuAttribute() =>
Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false),
"Missing [AuthorizeMenu].");

[TestMethod]
public void VendorController_HasAreaAttributeWithVendorArea()
{
var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(
typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false);
Assert.IsNotNull(areaAttr, "Missing [Area].");
Assert.AreEqual(VendorHost::Grand.Web.Vendor.Extensions.Constants.AreaVendor, areaAttr.RouteValue);
}

[TestMethod]
public void VendorController_HasAuthorizeVendorAttribute() =>
Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeVendorAttribute), false),
"Missing [AuthorizeVendor].");

[TestMethod]
public void VendorController_HasAuthorizeMenuAttribute() =>
Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false),
"Missing [AuthorizeMenu].");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using Grand.Business.Core.Interfaces.Catalog.Prices;
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.Common.Localization;
using Grand.Business.Core.Interfaces.Customers;
using Grand.Business.Core.Interfaces.Messages;
using Grand.Business.Core.Interfaces.Storage;
using Grand.Domain;
using Grand.Domain.Common;
using Grand.Domain.Customers;
using Grand.Domain.Directory;
using Grand.Domain.Localization;
using Grand.Domain.Orders;
using Grand.Infrastructure;
using Grand.Infrastructure.Configuration;
using Grand.Web.AdminShared.Interfaces;
using Grand.Web.AdminShared.Models.Orders;
using Grand.Web.AdminShared.Services;
using Grand.Web.Common.Localization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Grand.Web.Admin.Tests.Controllers;

[TestClass]
public class MerchandiseReturnViewModelServiceTests
{
[TestMethod]
public void InterfaceExposesRenamedMethod()
{
// Compile-time assertion: fails to build if PrepareReturnReqestListModel still exists
// instead of PrepareReturnRequestListModel (spec §2.4 typo fix).
IMerchandiseReturnViewModelService service = null;
Func<MerchandiseReturnListModel> _ = () => service.PrepareReturnRequestListModel();
}

[TestMethod]
public async Task PrepareMerchandiseReturnModel_RegisterCustomersPerStore_ScopesEmailLookupToCurrentStore()
{
var customerServiceMock = new Mock<ICustomerService>();
customerServiceMock
.Setup(c => c.GetCustomerByEmail("test@example.com", "store-1"))
.ReturnsAsync(new Customer { Id = "customer-1" });

var storeContextMock = new Mock<IStoreContext>();
storeContextMock.Setup(s => s.CurrentStore).Returns(new Grand.Domain.Stores.Store { Id = "store-1" });
var contextAccessorMock = new Mock<IContextAccessor>();
contextAccessorMock.Setup(c => c.StoreContext).Returns(storeContextMock.Object);

var customerConfig = new CustomerConfig { RegisterCustomersPerStore = true };

var service = Build(customerServiceMock.Object, contextAccessorMock.Object, customerConfig);

var model = new MerchandiseReturnListModel { SearchCustomerEmail = "Test@Example.com" };
await service.PrepareMerchandiseReturnModel(model, 1, 10);

// ToLowerInvariant() is kept (spec §2.4: AdminShared's existing lower-casing survives;
// Vendor's omission of it was the drift, not the other way around).
customerServiceMock.Verify(c => c.GetCustomerByEmail("test@example.com", "store-1"), Times.Once);
}

[TestMethod]
public async Task PrepareMerchandiseReturnModel_RegisterCustomersPerStoreDisabled_LooksUpAcrossAllStores()
{
var customerServiceMock = new Mock<ICustomerService>();
customerServiceMock
.Setup(c => c.GetCustomerByEmail("test@example.com", ""))
.ReturnsAsync(new Customer { Id = "customer-1" });

var contextAccessorMock = new Mock<IContextAccessor>();
var customerConfig = new CustomerConfig { RegisterCustomersPerStore = false };

var service = Build(customerServiceMock.Object, contextAccessorMock.Object, customerConfig);

var model = new MerchandiseReturnListModel { SearchCustomerEmail = "Test@Example.com" };
await service.PrepareMerchandiseReturnModel(model, 1, 10);

customerServiceMock.Verify(c => c.GetCustomerByEmail("test@example.com", ""), Times.Once);
}

[TestMethod]
public async Task PrepareMerchandiseReturnModel_ForwardsScopeDefaultStoreIdAndVendorIdToService()
{
// Regression test for coverage gap noted in Task 4: verify that scope.DefaultStoreId
// and scope.DefaultVendorId actually reach IMerchandiseReturnService.SearchMerchandiseReturns
// with the exact values (not just It.IsAny).
var merchandiseReturnServiceMock = new Mock<IMerchandiseReturnService>();
// Method signature: SearchMerchandiseReturns(storeId, customerId, orderItemId, vendorId, ownerId, rs, pageIndex, pageSize, createdFromUtc, createdToUtc)
merchandiseReturnServiceMock
.Setup(m => m.SearchMerchandiseReturns(
"store-42", It.IsAny<string>(), It.IsAny<string>(), "vendor-99", It.IsAny<string>(),
It.IsAny<MerchandiseReturnStatus?>(), It.IsAny<int>(), It.IsAny<int>(),
It.IsAny<DateTime?>(), It.IsAny<DateTime?>()))
.ReturnsAsync(new PagedList<MerchandiseReturn>());

var contextAccessorMock = new Mock<IContextAccessor>();
var scopeMock = new Mock<IAdminDataScope<MerchandiseReturn>>();
scopeMock.Setup(s => s.DefaultStoreId).Returns("store-42");
scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-99");

var service = BuildWithCustomMocks(
merchandiseReturnServiceMock.Object,
contextAccessorMock.Object,
scopeMock.Object);

var model = new MerchandiseReturnListModel();
await service.PrepareMerchandiseReturnModel(model, 1, 10);

// Verify exact call with scope values, not It.IsAny
merchandiseReturnServiceMock.Verify(m => m.SearchMerchandiseReturns(
"store-42", It.IsAny<string>(), It.IsAny<string>(), "vendor-99", It.IsAny<string>(),
It.IsAny<MerchandiseReturnStatus?>(), 0, 10,
It.IsAny<DateTime?>(), It.IsAny<DateTime?>()), Times.Once);
}

private static MerchandiseReturnViewModelService Build(ICustomerService customerService,
IContextAccessor contextAccessor, CustomerConfig customerConfig)
{
var orderServiceMock = new Mock<IOrderService>();
var productServiceMock = new Mock<IProductService>();
var dateTimeServiceMock = new Mock<IDateTimeService>();
dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc);
var translationServiceMock = new Mock<ITranslationService>();
var messageProviderServiceMock = new Mock<IMessageProviderService>();
var languageSettings = new LanguageSettings();
var merchandiseReturnServiceMock = new Mock<IMerchandiseReturnService>();
merchandiseReturnServiceMock
.Setup(m => m.SearchMerchandiseReturns(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<MerchandiseReturnStatus?>(), It.IsAny<int>(), It.IsAny<int>(),
It.IsAny<DateTime?>(), It.IsAny<DateTime?>()))
.ReturnsAsync(new PagedList<MerchandiseReturn>());
var priceFormatterMock = new Mock<IPriceFormatter>();
var addressSettings = new AddressSettings();
var countryServiceMock = new Mock<ICountryService>();
var addressAttributeServiceMock = new Mock<IAddressAttributeService>();
var addressAttributeParserMock = new Mock<IAddressAttributeParser>();
var downloadServiceMock = new Mock<IDownloadService>();
var orderSettings = new OrderSettings();
var enumTranslationServiceMock = new Mock<IEnumTranslationService>();
var scopeMock = new Mock<IAdminDataScope<MerchandiseReturn>>();
scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null);
scopeMock.Setup(s => s.DefaultVendorId).Returns((string)null);

return new MerchandiseReturnViewModelService(
orderServiceMock.Object,
productServiceMock.Object,
customerService,
dateTimeServiceMock.Object,
translationServiceMock.Object,
messageProviderServiceMock.Object,
languageSettings,
merchandiseReturnServiceMock.Object,
priceFormatterMock.Object,
addressSettings,
countryServiceMock.Object,
addressAttributeServiceMock.Object,
addressAttributeParserMock.Object,
downloadServiceMock.Object,
orderSettings,
enumTranslationServiceMock.Object,
contextAccessor,
customerConfig,
scopeMock.Object);
}

private static MerchandiseReturnViewModelService BuildWithCustomMocks(
IMerchandiseReturnService merchandiseReturnService,
IContextAccessor contextAccessor,
IAdminDataScope<MerchandiseReturn> scope)
{
var orderServiceMock = new Mock<IOrderService>();
var productServiceMock = new Mock<IProductService>();
var customerServiceMock = new Mock<ICustomerService>();
var dateTimeServiceMock = new Mock<IDateTimeService>();
dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc);
var translationServiceMock = new Mock<ITranslationService>();
var messageProviderServiceMock = new Mock<IMessageProviderService>();
var languageSettings = new LanguageSettings();
var priceFormatterMock = new Mock<IPriceFormatter>();
var addressSettings = new AddressSettings();
var countryServiceMock = new Mock<ICountryService>();
var addressAttributeServiceMock = new Mock<IAddressAttributeService>();
var addressAttributeParserMock = new Mock<IAddressAttributeParser>();
var downloadServiceMock = new Mock<IDownloadService>();
var orderSettings = new OrderSettings();
var enumTranslationServiceMock = new Mock<IEnumTranslationService>();
var customerConfig = new CustomerConfig();

return new MerchandiseReturnViewModelService(
orderServiceMock.Object,
productServiceMock.Object,
customerServiceMock.Object,
dateTimeServiceMock.Object,
translationServiceMock.Object,
messageProviderServiceMock.Object,
languageSettings,
merchandiseReturnService,
priceFormatterMock.Object,
addressSettings,
countryServiceMock.Object,
addressAttributeServiceMock.Object,
addressAttributeParserMock.Object,
downloadServiceMock.Object,
orderSettings,
enumTranslationServiceMock.Object,
contextAccessor,
customerConfig,
scope);
}
}
Loading
Loading