diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt index c57b50cc9..20446f9c0 100644 --- a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt +++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt @@ -1,5 +1,6 @@ { Id: prod-001, + IsReadOnly: false, AuctionEnded: false, VisibleIndividually: true, ProductLayoutId: pl-001, diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt index 7cd17a864..8a37d85e3 100644 --- a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt +++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt @@ -1,5 +1,6 @@ { Id: prod-002, + IsReadOnly: false, AuctionEnded: false, VisibleIndividually: false, Name: Calendar Product, diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseAddressAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseAddressAttributeControllerTests.cs index 129a8225a..24babb02c 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseAddressAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseAddressAttributeControllerTests.cs @@ -37,6 +37,7 @@ private class TestAddressAttributeController( private Mock _vmService = null!; private Mock _service = null!; private Mock> _scope = null!; + private Mock _languageService = null!; private TestAddressAttributeController _sut = null!; [TestInitialize] @@ -45,9 +46,10 @@ public void Setup() _vmService = new Mock(); _service = new Mock(); _scope = new Mock>(); + _languageService = new Mock(); _sut = new TestAddressAttributeController( _service.Object, _vmService.Object, - Mock.Of(), Mock.Of(), _scope.Object); + _languageService.Object, Mock.Of(), _scope.Object); var httpContext = new DefaultHttpContext(); var loggerFactoryMock = new Mock(); @@ -62,6 +64,26 @@ public void Setup() _sut.TempData = new TempDataDictionary(httpContext, new Mock().Object); } + [TestMethod] + public async Task EditPost_InvalidModel_KeepsThePostedLocales() + { + var entity = new AddressAttribute { Id = "1" }; + _service.Setup(x => x.GetAddressAttributeById("1")).ReturnsAsync(entity); + _scope.Setup(x => x.HasAccess(entity)).ReturnsAsync(true); + _languageService.Setup(l => l.GetAllLanguages(true, It.IsAny())) + .ReturnsAsync(new List { new() { Id = "lang-1" } }); + _sut.ModelState.AddModelError("Name", "required"); + var model = new AddressAttributeModel { + Id = "1", + Locales = [new AddressAttributeLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _sut.Edit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + [TestMethod] public async Task List_GlobalScope_ReturnsAllAttributesUnfiltered() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBlogControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBlogControllerTests.cs index ab7a4ed11..0d4d8530e 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBlogControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBlogControllerTests.cs @@ -46,6 +46,7 @@ private class TestBlogController( private Mock _translationServiceMock; private Mock> _postScopeMock; private Mock> _categoryScopeMock; + private Mock _languageServiceMock; [TestInitialize] public void Setup() @@ -67,14 +68,14 @@ public void Setup() _categoryScopeMock = new Mock>(); _categoryScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - var languageServiceMock = new Mock(); - languageServiceMock.Setup(l => l.GetAllLanguages(true, It.IsAny())) + _languageServiceMock = new Mock(); + _languageServiceMock.Setup(l => l.GetAllLanguages(true, It.IsAny())) .ReturnsAsync(new List()); _controller = new TestBlogController( _blogServiceMock.Object, _blogViewModelServiceMock.Object, - languageServiceMock.Object, + _languageServiceMock.Object, _translationServiceMock.Object, new Mock().Object, new Mock().Object, @@ -152,6 +153,63 @@ public async Task EditPost_ScopeDeniesAccess_RedirectsToEdit() _blogViewModelServiceMock.Verify(v => v.UpdateBlogPostModel(It.IsAny(), It.IsAny()), Times.Never); } + [TestMethod] + public async Task EditPost_InvalidModel_KeepsThePostedLocales() + { + var post = new BlogPost { Id = "p1" }; + _blogServiceMock.Setup(b => b.GetBlogPostById("p1")).ReturnsAsync(post); + _postScopeMock.Setup(s => s.HasAccess(post)).ReturnsAsync(true); + SetOneLanguage(); + _controller.ModelState.AddModelError("Title", "required"); + var model = new BlogPostModel { + Id = "p1", + Locales = [new BlogLocalizedModel { LanguageId = "lang-1", Title = "typed" }] + }; + + var result = await _controller.Edit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Title); + } + + [TestMethod] + public async Task CategoryCreatePost_InvalidModel_KeepsThePostedLocales() + { + SetOneLanguage(); + _controller.ModelState.AddModelError("Name", "required"); + var model = new BlogCategoryModel { + Locales = [new BlogCategoryLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _controller.CategoryCreate(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + + [TestMethod] + public async Task CategoryEditPost_InvalidModel_KeepsThePostedLocales() + { + var category = new BlogCategory { Id = "c1" }; + _blogServiceMock.Setup(b => b.GetBlogCategoryById("c1")).ReturnsAsync(category); + _categoryScopeMock.Setup(s => s.HasAccess(category)).ReturnsAsync(true); + SetOneLanguage(); + _controller.ModelState.AddModelError("Name", "required"); + var model = new BlogCategoryModel { + Id = "c1", + Locales = [new BlogCategoryLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _controller.CategoryEdit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + + private void SetOneLanguage() => + _languageServiceMock.Setup(l => l.GetAllLanguages(true, It.IsAny())) + .ReturnsAsync(new List { new() { Id = "lang-1" } }); + [TestMethod] public async Task Delete_ScopeDeniesAccess_RedirectsToEditWithoutDeleting() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAttributeControllerTests.cs index ff4ecf347..639de15d4 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAttributeControllerTests.cs @@ -34,6 +34,7 @@ private class TestCustomerAttributeController( private Mock _service = null!; private Mock _vmService = null!; private Mock> _scope = null!; + private Mock _languageService = null!; private TestCustomerAttributeController _sut = null!; [TestInitialize] @@ -42,8 +43,9 @@ public void Setup() _service = new Mock(); _vmService = new Mock(); _scope = new Mock>(); + _languageService = new Mock(); _sut = new TestCustomerAttributeController(_service.Object, _vmService.Object, - Mock.Of(), Mock.Of(), _scope.Object); + _languageService.Object, Mock.Of(), _scope.Object); var httpContext = new DefaultHttpContext(); var loggerFactoryMock = new Mock(); @@ -58,6 +60,26 @@ public void Setup() _sut.TempData = new TempDataDictionary(httpContext, new Mock().Object); } + [TestMethod] + public async Task EditPost_InvalidModel_KeepsThePostedLocales() + { + var entity = new CustomerAttribute { Id = "1" }; + _service.Setup(x => x.GetCustomerAttributeById("1")).ReturnsAsync(entity); + _scope.Setup(x => x.HasAccess(entity)).ReturnsAsync(true); + _languageService.Setup(l => l.GetAllLanguages(true, It.IsAny())) + .ReturnsAsync(new List { new() { Id = "lang-1" } }); + _sut.ModelState.AddModelError("Name", "required"); + var model = new CustomerAttributeModel { + Id = "1", + Locales = [new CustomerAttributeLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _sut.Edit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + [TestMethod] public async Task Delete_UsesViewModelServiceNotRawService() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs index e49c04221..a0d920cbd 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs @@ -213,28 +213,6 @@ public async Task CreatePost_GlobalScoped_LeavesModelStoresUntouched() await _controller.Create(submitted, false); } - [TestMethod] - public async Task CreateGet_PopulatesAllLanguages() - { - var languageServiceMock = new Mock(); - languageServiceMock.Setup(l => l.GetAllLanguages(true, It.IsAny())) - .ReturnsAsync(new List { new() { Id = "l1" } }); - var controller = new TestNewsController( - _newsViewModelServiceMock.Object, _newsServiceMock.Object, languageServiceMock.Object, - _translationServiceMock.Object, new Mock().Object, new Mock().Object, - _scopeMock.Object); - controller.ControllerContext = _controller.ControllerContext; - controller.TempData = _controller.TempData; - - await controller.Create(); - - // Confirms the value assigned to ViewBag.AllLanguages is the resolved list, not an unawaited - // Task object (the pre-existing bug this task fixes) - would throw InvalidCastException or - // fail this assertion if the bug were reintroduced. - var allLanguages = (IList)controller.ViewBag.AllLanguages; - Assert.AreEqual(1, allLanguages.Count); - } - [TestMethod] public async Task EditGet_NewsItemNotFound_RedirectsToList() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeControllerTests.cs index af9cc3890..40d8bfd19 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeControllerTests.cs @@ -38,6 +38,7 @@ private class TestProductAttributeController( private Mock _productService = null!; private Mock _attributeService = null!; private Mock> _scope = null!; + private Mock _languageService = null!; private TestProductAttributeController _sut = null!; [TestInitialize] @@ -46,8 +47,9 @@ public void Setup() _productService = new Mock(); _attributeService = new Mock(); _scope = new Mock>(); + _languageService = new Mock(); _sut = new TestProductAttributeController(_productService.Object, _attributeService.Object, - Mock.Of(), Mock.Of(), new SeoSettings(), _scope.Object); + _languageService.Object, Mock.Of(), new SeoSettings(), _scope.Object); var httpContext = new DefaultHttpContext(); var loggerFactoryMock = new Mock(); @@ -62,6 +64,26 @@ public void Setup() _sut.TempData = new TempDataDictionary(httpContext, new Mock().Object); } + [TestMethod] + public async Task EditPost_InvalidModel_KeepsThePostedLocales() + { + var entity = new ProductAttribute { Id = "1" }; + _attributeService.Setup(x => x.GetProductAttributeById("1")).ReturnsAsync(entity); + _scope.Setup(x => x.HasAccess(entity)).ReturnsAsync(true); + _languageService.Setup(l => l.GetAllLanguages(true, It.IsAny())) + .ReturnsAsync(new List { new() { Id = "lang-1" } }); + _sut.ModelState.AddModelError("Name", "required"); + var model = new Grand.Web.AdminShared.Models.Catalog.ProductAttributeModel { + Id = "1", + Locales = [new Grand.Web.AdminShared.Models.Catalog.ProductAttributeLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _sut.Edit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + [TestMethod] public async Task List_PassesScopeStoreIdToService() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs index 3492ba202..d464e981f 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs @@ -17,11 +17,17 @@ using Grand.Web.Common.Security.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using System.Globalization; using System.Linq.Expressions; namespace Grand.Web.Admin.Tests.Controllers; @@ -140,7 +146,7 @@ public async Task EditGet_UsesCanViewNotHasAccess_LooserRuleWins() //shown, but read-only: HasAccess no longer gates the page, it only takes the buttons away Assert.IsInstanceOfType(result); - Assert.AreEqual(true, ((ViewResult)result).ViewData["IsReadOnly"]); + Assert.IsTrue(((ProductModel)((ViewResult)result).Model).IsReadOnly); } [TestMethod] @@ -154,7 +160,36 @@ public async Task EditGet_OwnedProduct_IsNotReadOnly() var result = await _controller.Edit("p1"); Assert.IsInstanceOfType(result); - Assert.AreEqual(false, ((ViewResult)result).ViewData["IsReadOnly"]); + Assert.IsFalse(((ProductModel)((ViewResult)result).Model).IsReadOnly); + } + + [TestMethod] + public async Task IsReadOnly_IsNeverBoundFromTheRequest() + { + // The server decides it in Edit(GET); a posted value must not reach the re-rendered form. + var services = new ServiceCollection().AddLogging(); + services.AddMvcCore(); + await using var provider = services.BuildServiceProvider(); + var metadata = provider.GetRequiredService().GetMetadataForType(typeof(ProductModel)); + var binder = provider.GetRequiredService().CreateBinder( + new ModelBinderFactoryContext { Metadata = metadata, BindingInfo = new BindingInfo() }); + var form = new FormCollection(new Dictionary { + ["Name"] = "posted", + ["IsReadOnly"] = "true" + }); + + var result = await provider.GetRequiredService().BindModelAsync( + new ActionContext(new DefaultHttpContext { RequestServices = provider }, new RouteData(), new ActionDescriptor()), + binder, + new FormValueProvider(BindingSource.Form, form, CultureInfo.InvariantCulture), + new ParameterDescriptor { Name = "model", ParameterType = typeof(ProductModel) }, + metadata, + value: null, + container: null); + + var model = (ProductModel)result.Model; + Assert.AreEqual("posted", model.Name); + Assert.IsFalse(model.IsReadOnly); } // --- Edit (POST) ------------------------------------------------------------------------------- @@ -473,11 +508,7 @@ public async Task LoadProductFriendlyNames_ScopeDeniesAccess_SkipsProduct() var json = result as JsonResult; Assert.IsNotNull(json); - // Faithful port of Store/Vendor's original index-based comma logic: it decides whether to - // append ", " from the loop index (i != products.Count - 1), not from whether a name was - // actually appended, so skipping the last product still leaves a trailing ", ". Pre-existing - // quirk in both original hosts, not introduced by this migration - characterized, not fixed. - Assert.AreEqual("Allowed, ", GetTextProperty(json.Value)); + Assert.AreEqual("Allowed", GetTextProperty(json.Value)); } [TestMethod] @@ -508,11 +539,10 @@ public async Task RequiredProductAddPopup_CallsPrepareAddRequiredProductModel() _productAddPopupViewModelServiceMock.Setup(s => s.PrepareAddRequiredProductModel()) .ReturnsAsync(new ProductModel.AddRequiredProductModel()); - var result = await _controller.RequiredProductAddPopup("input1") as ViewResult; + var result = await _controller.RequiredProductAddPopup() as ViewResult; Assert.IsNotNull(result); _productAddPopupViewModelServiceMock.Verify(s => s.PrepareAddRequiredProductModel(), Times.Once); - Assert.AreEqual("input1", _controller.ViewBag.productIdsInput); } // --- RequiredProductAddPopupList ------------------------------------------------------------------- diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs index 5fc3f587a..785c048f2 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs @@ -38,6 +38,7 @@ private class TestSpecificationAttributeController( private Mock _service = null!; private Mock _productService = null!; private Mock> _scope = null!; + private Mock _languageService = null!; private TestSpecificationAttributeController _sut = null!; [TestInitialize] @@ -46,7 +47,8 @@ public void Setup() _service = new Mock(); _productService = new Mock(); _scope = new Mock>(); - _sut = new TestSpecificationAttributeController(_service.Object, Mock.Of(), + _languageService = new Mock(); + _sut = new TestSpecificationAttributeController(_service.Object, _languageService.Object, Mock.Of(), _productService.Object, new SeoSettings(), _scope.Object); var httpContext = new DefaultHttpContext(); @@ -62,6 +64,26 @@ public void Setup() _sut.TempData = new TempDataDictionary(httpContext, new Mock().Object); } + [TestMethod] + public async Task EditPost_InvalidModel_KeepsThePostedLocales() + { + var entity = new SpecificationAttribute { Id = "1" }; + _service.Setup(x => x.GetSpecificationAttributeById("1")).ReturnsAsync(entity); + _scope.Setup(x => x.HasAccess(entity)).ReturnsAsync(true); + _languageService.Setup(l => l.GetAllLanguages(true, It.IsAny())) + .ReturnsAsync(new List { new() { Id = "lang-1" } }); + _sut.ModelState.AddModelError("Name", "required"); + var model = new Grand.Web.AdminShared.Models.Catalog.SpecificationAttributeModel { + Id = "1", + Locales = [new Grand.Web.AdminShared.Models.Catalog.SpecificationAttributeLocalizedModel { LanguageId = "lang-1", Name = "typed" }] + }; + + var result = await _sut.Edit(model, false); + + Assert.IsInstanceOfType(result); + Assert.AreEqual("typed", model.Locales.Single().Name); + } + [TestMethod] public async Task List_PassesScopeStoreIdToService() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderTagsControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderTagsControllerTests.cs new file mode 100644 index 000000000..bff8cc80c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderTagsControllerTests.cs @@ -0,0 +1,90 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Domain.Orders; +using Grand.Web.Admin.Controllers; +using Grand.Web.AdminShared.Models.Orders; +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; + +[TestClass] +public class OrderTagsControllerTests +{ + private OrderTagsController _controller; + private Mock _orderTagServiceMock; + private Mock _translationServiceMock; + + [TestInitialize] + public void Setup() + { + _orderTagServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + _controller = new OrderTagsController( + _orderTagServiceMock.Object, + new Mock().Object, + new Mock().Object, + _translationServiceMock.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); + } + + [TestMethod] + public async Task EditPost_ValidModel_SavesAndRedirectsToEditWithSuccess() + { + var tag = new OrderTag { Id = "t1", Name = "old" }; + _orderTagServiceMock.Setup(s => s.GetOrderTagById("t1")).ReturnsAsync(tag); + + var result = await _controller.Edit(new OrderTagModel { Id = "t1", Name = "new" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Edit", redirect.ActionName); + Assert.AreEqual("t1", redirect.RouteValues["id"]); + _orderTagServiceMock.Verify(s => s.UpdateOrderTag(It.Is(t => t.Name == "new")), Times.Once); + _translationServiceMock.Verify(t => t.GetResource("Admin.Orders.OrderTags.Updated"), Times.Once); + } + + [TestMethod] + public async Task EditPost_InvalidModel_RedisplaysTheFormWithoutSaving() + { + _orderTagServiceMock.Setup(s => s.GetOrderTagById("t1")).ReturnsAsync(new OrderTag { Id = "t1", Name = "old" }); + _controller.ModelState.AddModelError("Name", "required"); + + var result = await _controller.Edit(new OrderTagModel { Id = "t1" }); + + Assert.IsInstanceOfType(result); + _orderTagServiceMock.Verify(s => s.UpdateOrderTag(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task EditPost_TagNotFound_RedirectsToListWithoutSaving() + { + _orderTagServiceMock.Setup(s => s.GetOrderTagById("missing")).ReturnsAsync((OrderTag)null); + + var result = await _controller.Edit(new OrderTagModel { Id = "missing", Name = "new" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _orderTagServiceMock.Verify(s => s.UpdateOrderTag(It.IsAny()), Times.Never); + } +} diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Course/AssociateProductToCoursePopup.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Course/AssociateProductToCoursePopup.cshtml index 5e3035710..0f097740a 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Course/AssociateProductToCoursePopup.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Course/AssociateProductToCoursePopup.cshtml @@ -51,9 +51,9 @@ {