From 9882a2290ba50edbbd81791ade55231689d66693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:04:23 +0000 Subject: [PATCH 1/2] Initial plan From 10bef2d14c10b194c370c344f078328b91b7b670 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:13:28 +0000 Subject: [PATCH 2/2] fix: address validation only fires when address fields are updated - Remove [Required] from Address, City, Country, PostalCode fields in UserPersonalUpdateModel - Implement IValidatableObject with conditional validation: if any address field is provided, all required address fields (Address, City, Country, PostalCode) must be filled - Add HasAnyAddressField() helper method - Fix OnPostUpdatePersonalInfoAsync to only call UpdateShippingAddress when address fields are present - Add validation summary div to Admin/User.cshtml personal info form - Add unit tests for HasAnyAddressField and conditional address validation Closes #1422 --- src/DevBetterWeb.Web/Pages/Admin/User.cshtml | 1 + .../Pages/Admin/User.cshtml.cs | 5 +- .../Pages/User/UserPersonalUpdateModel.cs | 33 ++++-- .../UserPersonalUpdateModelValidateTests.cs | 100 ++++++++++++++++++ 4 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 tests/DevBetterWeb.UnitTests/Web/Models/UserPersonalUpdateModelValidateTests.cs diff --git a/src/DevBetterWeb.Web/Pages/Admin/User.cshtml b/src/DevBetterWeb.Web/Pages/Admin/User.cshtml index 012b135ca..17c02bce9 100644 --- a/src/DevBetterWeb.Web/Pages/Admin/User.cshtml +++ b/src/DevBetterWeb.Web/Pages/Admin/User.cshtml @@ -128,6 +128,7 @@
+
diff --git a/src/DevBetterWeb.Web/Pages/Admin/User.cshtml.cs b/src/DevBetterWeb.Web/Pages/Admin/User.cshtml.cs index bf9e48aac..3272d5a6b 100644 --- a/src/DevBetterWeb.Web/Pages/Admin/User.cshtml.cs +++ b/src/DevBetterWeb.Web/Pages/Admin/User.cshtml.cs @@ -272,7 +272,10 @@ public async Task OnPostUpdatePersonalInfoAsync(string userId) member.UpdatePEInfo(UserPersonalUpdateModel.PEFriendCode, UserPersonalUpdateModel.PEUsername, false); member.UpdateAboutInfo(UserPersonalUpdateModel.AboutInfo, false); member.UpdateAddress(UserPersonalUpdateModel.Address, false); - member.UpdateShippingAddress(UserPersonalUpdateModel.Address!, UserPersonalUpdateModel.City!, UserPersonalUpdateModel.State!, UserPersonalUpdateModel.PostalCode!, UserPersonalUpdateModel.Country!, false); + if (UserPersonalUpdateModel.HasAnyAddressField()) + { + member.UpdateShippingAddress(UserPersonalUpdateModel.Address!, UserPersonalUpdateModel.City!, UserPersonalUpdateModel.State!, UserPersonalUpdateModel.PostalCode!, UserPersonalUpdateModel.Country!, false); + } member.UpdateDiscord(UserPersonalUpdateModel.DiscordUsername, false); member.UpdateEmail(UserPersonalUpdateModel.Email, false); diff --git a/src/DevBetterWeb.Web/Pages/User/UserPersonalUpdateModel.cs b/src/DevBetterWeb.Web/Pages/User/UserPersonalUpdateModel.cs index 516e68509..98104c567 100644 --- a/src/DevBetterWeb.Web/Pages/User/UserPersonalUpdateModel.cs +++ b/src/DevBetterWeb.Web/Pages/User/UserPersonalUpdateModel.cs @@ -1,24 +1,21 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.Metrics; using DevBetterWeb.Core.Entities; namespace DevBetterWeb.Web.Pages.User; -public class UserPersonalUpdateModel +public class UserPersonalUpdateModel : IValidatableObject { [Required] public string? FirstName { get; set; } [Required] public string? LastName { get; set; } - [Required] public string? Address { get; set; } - [Required] public string? City { get; set; } public string? State { get; set; } - [Required] public string? Country { get; set; } - [Required] public string? PostalCode { get; set; } [Range(1, 31)] [BirthdayDay] @@ -60,4 +57,28 @@ public UserPersonalUpdateModel(Member member) PEUsername = member.PEUsername; DiscordUsername = member.DiscordUsername; } + + public bool HasAnyAddressField() => + !string.IsNullOrWhiteSpace(Address) || + !string.IsNullOrWhiteSpace(City) || + !string.IsNullOrWhiteSpace(Country) || + !string.IsNullOrWhiteSpace(PostalCode) || + !string.IsNullOrWhiteSpace(State); + + public IEnumerable Validate(ValidationContext validationContext) + { + if (!HasAnyAddressField()) yield break; + + if (string.IsNullOrWhiteSpace(Address)) + yield return new ValidationResult("The Address field is required when providing address information.", new[] { nameof(Address) }); + + if (string.IsNullOrWhiteSpace(City)) + yield return new ValidationResult("The City field is required when providing address information.", new[] { nameof(City) }); + + if (string.IsNullOrWhiteSpace(Country)) + yield return new ValidationResult("The Country field is required when providing address information.", new[] { nameof(Country) }); + + if (string.IsNullOrWhiteSpace(PostalCode)) + yield return new ValidationResult("The Postal Code field is required when providing address information.", new[] { nameof(PostalCode) }); + } } diff --git a/tests/DevBetterWeb.UnitTests/Web/Models/UserPersonalUpdateModelValidateTests.cs b/tests/DevBetterWeb.UnitTests/Web/Models/UserPersonalUpdateModelValidateTests.cs new file mode 100644 index 000000000..45359064b --- /dev/null +++ b/tests/DevBetterWeb.UnitTests/Web/Models/UserPersonalUpdateModelValidateTests.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using DevBetterWeb.Web.Pages.User; +using Xunit; + +namespace DevBetterWeb.UnitTests.Web.Models; + +public class UserPersonalUpdateModelValidateTests +{ + private static List Validate(UserPersonalUpdateModel model) + { + var results = new List(); + var context = new ValidationContext(model); + Validator.TryValidateObject(model, context, results, validateAllProperties: true); + return results; + } + + [Fact] + public void NoAddressFields_NoValidationErrors() + { + var model = new UserPersonalUpdateModel + { + FirstName = "Jane", + LastName = "Doe", + Email = "jane@example.com" + }; + + var errors = Validate(model); + + Assert.Empty(errors); + } + + [Fact] + public void AllAddressFieldsProvided_NoValidationErrors() + { + var model = new UserPersonalUpdateModel + { + FirstName = "Jane", + LastName = "Doe", + Address = "123 Main St", + City = "Springfield", + Country = "US", + PostalCode = "12345", + State = "IL" + }; + + var errors = Validate(model); + + Assert.Empty(errors); + } + + [Fact] + public void OnlyCityProvided_ValidationErrorsForOtherAddressFields() + { + var model = new UserPersonalUpdateModel + { + FirstName = "Jane", + LastName = "Doe", + City = "Springfield" + }; + + var errors = Validate(model); + + Assert.Contains(errors, e => e.MemberNames != null && System.Linq.Enumerable.Contains(e.MemberNames, nameof(UserPersonalUpdateModel.Address))); + Assert.Contains(errors, e => e.MemberNames != null && System.Linq.Enumerable.Contains(e.MemberNames, nameof(UserPersonalUpdateModel.Country))); + Assert.Contains(errors, e => e.MemberNames != null && System.Linq.Enumerable.Contains(e.MemberNames, nameof(UserPersonalUpdateModel.PostalCode))); + } + + [Fact] + public void HasAnyAddressField_ReturnsFalseWhenAllEmpty() + { + var model = new UserPersonalUpdateModel(); + + Assert.False(model.HasAnyAddressField()); + } + + [Fact] + public void HasAnyAddressField_ReturnsTrueWhenAddressSet() + { + var model = new UserPersonalUpdateModel { Address = "123 Main St" }; + + Assert.True(model.HasAnyAddressField()); + } + + [Fact] + public void HasAnyAddressField_ReturnsTrueWhenOnlyCitySet() + { + var model = new UserPersonalUpdateModel { City = "Springfield" }; + + Assert.True(model.HasAnyAddressField()); + } + + [Fact] + public void HasAnyAddressField_ReturnsFalseForWhitespaceOnly() + { + var model = new UserPersonalUpdateModel { Address = " ", City = " " }; + + Assert.False(model.HasAnyAddressField()); + } +}