1212import com .digitalsanctuary .spring .user .audit .AuditEvent ;
1313import com .digitalsanctuary .spring .user .dto .PasswordDto ;
1414import com .digitalsanctuary .spring .user .dto .UserDto ;
15+ import com .digitalsanctuary .spring .user .dto .UserProfileUpdateDto ;
1516import com .digitalsanctuary .spring .user .event .OnRegistrationCompleteEvent ;
1617import com .digitalsanctuary .spring .user .exceptions .InvalidOldPasswordException ;
1718import com .digitalsanctuary .spring .user .exceptions .UserAlreadyExistException ;
4546import org .springframework .web .bind .annotation .ExceptionHandler ;
4647import org .springframework .web .bind .annotation .RestControllerAdvice ;
4748import org .springframework .http .HttpStatus ;
49+ import org .springframework .validation .beanvalidation .LocalValidatorFactoryBean ;
4850
4951import java .util .Collections ;
5052import 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