Skip to content

Commit 3b8bf6f

Browse files
devondragonclaude
andcommitted
Fix UserAPIUnitTest for UserProfileUpdateDto and add validation tests
- Update existing profile update tests to use UserProfileUpdateDto - Add comprehensive validation tests for UserProfileUpdateDto: - Blank firstName validation - Blank lastName validation - firstName exceeding 50 character limit - Null fields validation - Maximum valid length (50 chars) acceptance - Add hibernate-validator to test dependencies for proper validation - Fix testUserDto setup to include matchingPassword field - Update missing email/password tests to expect 400 (validation error) instead of 500 (internal error) - Update CSRF test to reflect standalone MockMvc limitations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 69f044a commit 3b8bf6f

2 files changed

Lines changed: 229 additions & 20 deletions

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dependencies {
7171
testImplementation 'org.springframework.security:spring-security-test'
7272
testImplementation 'org.springframework.retry:spring-retry:2.0.12'
7373
testImplementation 'jakarta.validation:jakarta.validation-api:3.1.1'
74+
testImplementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final'
7475
testImplementation 'com.h2database:h2:2.4.240'
7576

7677
// Spring Boot 4 test starters (modular test infrastructure)

src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java

Lines changed: 228 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.digitalsanctuary.spring.user.audit.AuditEvent;
1313
import com.digitalsanctuary.spring.user.dto.PasswordDto;
1414
import com.digitalsanctuary.spring.user.dto.UserDto;
15+
import com.digitalsanctuary.spring.user.dto.UserProfileUpdateDto;
1516
import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent;
1617
import com.digitalsanctuary.spring.user.exceptions.InvalidOldPasswordException;
1718
import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException;
@@ -45,6 +46,7 @@
4546
import org.springframework.web.bind.annotation.ExceptionHandler;
4647
import org.springframework.web.bind.annotation.RestControllerAdvice;
4748
import org.springframework.http.HttpStatus;
49+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
4850

4951
import java.util.Collections;
5052
import java.util.Locale;
@@ -112,6 +114,7 @@ void setUp() {
112114
testUserDto.setFirstName("Test");
113115
testUserDto.setLastName("User");
114116
testUserDto.setPassword("password123");
117+
testUserDto.setMatchingPassword("password123");
115118
testUserDto.setRole(1);
116119

117120
testUserDetails = new DSUserDetails(testUser);
@@ -227,14 +230,12 @@ void registerUserAccount_missingEmail() throws Exception {
227230
// Given
228231
testUserDto.setEmail(null);
229232

230-
// When & Then
233+
// When & Then - validation should reject null email with 400 Bad Request
231234
mockMvc.perform(post("/user/registration")
232235
.contentType(MediaType.APPLICATION_JSON)
233236
.content(objectMapper.writeValueAsString(testUserDto))
234237
.with(csrf()))
235-
.andExpect(status().isInternalServerError())
236-
.andExpect(jsonPath("$.success").value(false))
237-
.andExpect(jsonPath("$.code").value(5));
238+
.andExpect(status().isBadRequest());
238239
}
239240

240241
@Test
@@ -243,14 +244,12 @@ void registerUserAccount_missingPassword() throws Exception {
243244
// Given
244245
testUserDto.setPassword(null);
245246

246-
// When & Then
247+
// When & Then - validation should reject null password with 400 Bad Request
247248
mockMvc.perform(post("/user/registration")
248249
.contentType(MediaType.APPLICATION_JSON)
249250
.content(objectMapper.writeValueAsString(testUserDto))
250251
.with(csrf()))
251-
.andExpect(status().isInternalServerError())
252-
.andExpect(jsonPath("$.success").value(false))
253-
.andExpect(jsonPath("$.code").value(5));
252+
.andExpect(status().isBadRequest());
254253
}
255254

256255
@Test
@@ -487,7 +486,7 @@ class UserProfileTests {
487486
@DisplayName("POST /user/updateUser - success")
488487
void updateUser_success() throws Exception {
489488
// Given
490-
UserDto updateDto = new UserDto();
489+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
491490
updateDto.setFirstName("UpdatedFirst");
492491
updateDto.setLastName("UpdatedLast");
493492

@@ -533,7 +532,7 @@ public Object resolveArgument(org.springframework.core.MethodParameter parameter
533532
@DisplayName("POST /user/updateUser - not authenticated")
534533
void updateUser_notAuthenticated() throws Exception {
535534
// Given
536-
UserDto updateDto = new UserDto();
535+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
537536
updateDto.setFirstName("UpdatedFirst");
538537
updateDto.setLastName("UpdatedLast");
539538

@@ -547,6 +546,215 @@ void updateUser_notAuthenticated() throws Exception {
547546
.andExpect(jsonPath("$.code").value(401))
548547
.andExpect(jsonPath("$.messages[0]").value("User not logged in."));
549548
}
549+
550+
@Test
551+
@DisplayName("POST /user/updateUser - validation fails with blank firstName")
552+
void updateUser_blankFirstName_fails() throws Exception {
553+
// Given
554+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
555+
updateDto.setFirstName(""); // Blank - should fail validation
556+
updateDto.setLastName("UpdatedLast");
557+
558+
// Create a validator for the standalone setup
559+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
560+
validator.afterPropertiesSet();
561+
562+
// Mock the principal resolver to return our test user
563+
mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
564+
.setValidator(validator)
565+
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
566+
@Override
567+
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
568+
return parameter.getParameterType().equals(DSUserDetails.class);
569+
}
570+
571+
@Override
572+
public Object resolveArgument(org.springframework.core.MethodParameter parameter,
573+
org.springframework.web.method.support.ModelAndViewContainer mavContainer,
574+
org.springframework.web.context.request.NativeWebRequest webRequest,
575+
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
576+
return testUserDetails;
577+
}
578+
})
579+
.setControllerAdvice(new TestExceptionHandler())
580+
.build();
581+
582+
// When & Then - validation should fail
583+
mockMvc.perform(post("/user/updateUser")
584+
.contentType(MediaType.APPLICATION_JSON)
585+
.content(objectMapper.writeValueAsString(updateDto))
586+
.with(csrf()))
587+
.andExpect(status().isBadRequest());
588+
589+
verify(userService, never()).saveRegisteredUser(any(User.class));
590+
}
591+
592+
@Test
593+
@DisplayName("POST /user/updateUser - validation fails with blank lastName")
594+
void updateUser_blankLastName_fails() throws Exception {
595+
// Given
596+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
597+
updateDto.setFirstName("UpdatedFirst");
598+
updateDto.setLastName(""); // Blank - should fail validation
599+
600+
// Create a validator for the standalone setup
601+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
602+
validator.afterPropertiesSet();
603+
604+
// Mock the principal resolver to return our test user
605+
mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
606+
.setValidator(validator)
607+
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
608+
@Override
609+
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
610+
return parameter.getParameterType().equals(DSUserDetails.class);
611+
}
612+
613+
@Override
614+
public Object resolveArgument(org.springframework.core.MethodParameter parameter,
615+
org.springframework.web.method.support.ModelAndViewContainer mavContainer,
616+
org.springframework.web.context.request.NativeWebRequest webRequest,
617+
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
618+
return testUserDetails;
619+
}
620+
})
621+
.setControllerAdvice(new TestExceptionHandler())
622+
.build();
623+
624+
// When & Then - validation should fail
625+
mockMvc.perform(post("/user/updateUser")
626+
.contentType(MediaType.APPLICATION_JSON)
627+
.content(objectMapper.writeValueAsString(updateDto))
628+
.with(csrf()))
629+
.andExpect(status().isBadRequest());
630+
631+
verify(userService, never()).saveRegisteredUser(any(User.class));
632+
}
633+
634+
@Test
635+
@DisplayName("POST /user/updateUser - validation fails with firstName exceeding 50 characters")
636+
void updateUser_firstNameTooLong_fails() throws Exception {
637+
// Given
638+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
639+
updateDto.setFirstName("A".repeat(51)); // 51 chars - exceeds 50 char limit
640+
updateDto.setLastName("UpdatedLast");
641+
642+
// Create a validator for the standalone setup
643+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
644+
validator.afterPropertiesSet();
645+
646+
// Mock the principal resolver to return our test user
647+
mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
648+
.setValidator(validator)
649+
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
650+
@Override
651+
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
652+
return parameter.getParameterType().equals(DSUserDetails.class);
653+
}
654+
655+
@Override
656+
public Object resolveArgument(org.springframework.core.MethodParameter parameter,
657+
org.springframework.web.method.support.ModelAndViewContainer mavContainer,
658+
org.springframework.web.context.request.NativeWebRequest webRequest,
659+
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
660+
return testUserDetails;
661+
}
662+
})
663+
.setControllerAdvice(new TestExceptionHandler())
664+
.build();
665+
666+
// When & Then - validation should fail
667+
mockMvc.perform(post("/user/updateUser")
668+
.contentType(MediaType.APPLICATION_JSON)
669+
.content(objectMapper.writeValueAsString(updateDto))
670+
.with(csrf()))
671+
.andExpect(status().isBadRequest());
672+
673+
verify(userService, never()).saveRegisteredUser(any(User.class));
674+
}
675+
676+
@Test
677+
@DisplayName("POST /user/updateUser - validation fails with null fields")
678+
void updateUser_nullFields_fails() throws Exception {
679+
// Given
680+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
681+
// Both fields are null - should fail validation
682+
683+
// Create a validator for the standalone setup
684+
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
685+
validator.afterPropertiesSet();
686+
687+
// Mock the principal resolver to return our test user
688+
mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
689+
.setValidator(validator)
690+
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
691+
@Override
692+
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
693+
return parameter.getParameterType().equals(DSUserDetails.class);
694+
}
695+
696+
@Override
697+
public Object resolveArgument(org.springframework.core.MethodParameter parameter,
698+
org.springframework.web.method.support.ModelAndViewContainer mavContainer,
699+
org.springframework.web.context.request.NativeWebRequest webRequest,
700+
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
701+
return testUserDetails;
702+
}
703+
})
704+
.setControllerAdvice(new TestExceptionHandler())
705+
.build();
706+
707+
// When & Then - validation should fail
708+
mockMvc.perform(post("/user/updateUser")
709+
.contentType(MediaType.APPLICATION_JSON)
710+
.content(objectMapper.writeValueAsString(updateDto))
711+
.with(csrf()))
712+
.andExpect(status().isBadRequest());
713+
714+
verify(userService, never()).saveRegisteredUser(any(User.class));
715+
}
716+
717+
@Test
718+
@DisplayName("POST /user/updateUser - accepts maximum valid length names")
719+
void updateUser_maxValidLength_succeeds() throws Exception {
720+
// Given
721+
UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
722+
updateDto.setFirstName("A".repeat(50)); // Exactly 50 chars - should be valid
723+
updateDto.setLastName("B".repeat(50)); // Exactly 50 chars - should be valid
724+
725+
// Mock the principal resolver to return our test user
726+
mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
727+
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
728+
@Override
729+
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
730+
return parameter.getParameterType().equals(DSUserDetails.class);
731+
}
732+
733+
@Override
734+
public Object resolveArgument(org.springframework.core.MethodParameter parameter,
735+
org.springframework.web.method.support.ModelAndViewContainer mavContainer,
736+
org.springframework.web.context.request.NativeWebRequest webRequest,
737+
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
738+
return testUserDetails;
739+
}
740+
})
741+
.setControllerAdvice(new TestExceptionHandler())
742+
.build();
743+
744+
when(messageSource.getMessage(eq("message.update-user.success"), any(), any(Locale.class)))
745+
.thenReturn("Profile updated successfully");
746+
when(userService.saveRegisteredUser(any(User.class))).thenReturn(testUser);
747+
748+
// When & Then
749+
mockMvc.perform(post("/user/updateUser")
750+
.contentType(MediaType.APPLICATION_JSON)
751+
.content(objectMapper.writeValueAsString(updateDto))
752+
.with(csrf()))
753+
.andExpect(status().isOk())
754+
.andExpect(jsonPath("$.success").value(true));
755+
756+
verify(userService).saveRegisteredUser(any(User.class));
757+
}
550758
}
551759

552760
@Nested
@@ -603,21 +811,21 @@ void deleteAccount_notAuthenticated() throws Exception {
603811
class SecurityValidationTests {
604812

605813
@Test
606-
@DisplayName("POST /user/registration - CSRF protection")
814+
@DisplayName("POST /user/registration - CSRF protection (standalone MockMvc limitation)")
607815
void registration_csrfProtection() throws Exception {
608-
// Note: In standalone MockMvc setup, CSRF protection is not enabled
609-
// This test would pass with @WebMvcTest but not with standalone setup
610-
// For now, we skip this test for standalone unit testing
611-
// CSRF protection should be tested in integration tests instead
612-
613-
// Given - simulating missing required fields to get an error
816+
// Note: In standalone MockMvc setup, CSRF protection is not enabled by default.
817+
// This test verifies basic request handling. Actual CSRF protection should be
818+
// tested in integration tests using @WebMvcTest or full Spring context.
819+
820+
// Given - simulating missing required fields to trigger validation error
614821
testUserDto.setEmail(null);
615-
616-
// When & Then
822+
823+
// When & Then - without CSRF token, request still reaches validation
824+
// which fails with 400 Bad Request for missing email
617825
mockMvc.perform(post("/user/registration")
618826
.contentType(MediaType.APPLICATION_JSON)
619827
.content(objectMapper.writeValueAsString(testUserDto)))
620-
.andExpect(status().is5xxServerError());
828+
.andExpect(status().isBadRequest());
621829
}
622830

623831
@Test

0 commit comments

Comments
 (0)