Controller (7 tests): signup, login, logout, GET/PATCH me. Standalone MockMvc setup (Boot 4 removed @WebMvcTest). Service (11 tests): signup with conflict check, login with password/active validation, getCurrentUser with household info, updateProfile with password change flow. Repository (4 tests): save/find, case-insensitive email via IgnoreCase queries (citext + Hibernate needs explicit IgnoreCase), existsByEmail. Also includes: - SecurityConfig: session auth, CSRF, role-based authorization - CustomUserDetailsService: loads UserAccount for Spring Security - UserAccount, Household, HouseholdMember JPA entities - spring-boot-flyway dependency (Boot 4 requires explicit module) - ddl-auto=none (Flyway owns schema, validate fails on citext) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
7.5 KiB
Java
184 lines
7.5 KiB
Java
package com.recipeapp.auth;
|
|
|
|
import com.recipeapp.auth.dto.*;
|
|
import com.recipeapp.auth.entity.UserAccount;
|
|
import com.recipeapp.common.ConflictException;
|
|
import com.recipeapp.common.ResourceNotFoundException;
|
|
import com.recipeapp.common.ValidationException;
|
|
import com.recipeapp.household.HouseholdMemberRepository;
|
|
import com.recipeapp.household.entity.Household;
|
|
import com.recipeapp.household.entity.HouseholdMember;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
|
import java.util.Optional;
|
|
|
|
import static org.assertj.core.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
class AuthServiceTest {
|
|
|
|
@Mock
|
|
private UserAccountRepository userAccountRepository;
|
|
|
|
@Mock
|
|
private HouseholdMemberRepository householdMemberRepository;
|
|
|
|
@Mock
|
|
private PasswordEncoder passwordEncoder;
|
|
|
|
@InjectMocks
|
|
private AuthServiceImpl authService;
|
|
|
|
@Test
|
|
void signupShouldCreateUserAndReturnResponse() {
|
|
var request = new SignupRequest("sarah@example.com", "s3cure!Pass", "Sarah");
|
|
|
|
when(userAccountRepository.existsByEmailIgnoreCase("sarah@example.com")).thenReturn(false);
|
|
when(passwordEncoder.encode("s3cure!Pass")).thenReturn("hashed");
|
|
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(invocation -> {
|
|
UserAccount saved = invocation.getArgument(0);
|
|
return saved;
|
|
});
|
|
|
|
UserResponse result = authService.signup(request);
|
|
|
|
assertThat(result.email()).isEqualTo("sarah@example.com");
|
|
assertThat(result.displayName()).isEqualTo("Sarah");
|
|
verify(userAccountRepository).save(any(UserAccount.class));
|
|
}
|
|
|
|
@Test
|
|
void signupShouldThrowConflictWhenEmailExists() {
|
|
var request = new SignupRequest("sarah@example.com", "s3cure!Pass", "Sarah");
|
|
|
|
when(userAccountRepository.existsByEmailIgnoreCase("sarah@example.com")).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> authService.signup(request))
|
|
.isInstanceOf(ConflictException.class);
|
|
}
|
|
|
|
@Test
|
|
void loginShouldReturnUserWhenCredentialsValid() {
|
|
var request = new LoginRequest("sarah@example.com", "s3cure!Pass");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(passwordEncoder.matches("s3cure!Pass", "hashed")).thenReturn(true);
|
|
when(householdMemberRepository.findByUserEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.empty());
|
|
|
|
UserResponse result = authService.login(request);
|
|
|
|
assertThat(result.email()).isEqualTo("sarah@example.com");
|
|
assertThat(result.displayName()).isEqualTo("Sarah");
|
|
}
|
|
|
|
@Test
|
|
void loginShouldThrowWhenEmailNotFound() {
|
|
var request = new LoginRequest("unknown@example.com", "password");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("unknown@example.com")).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> authService.login(request))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void loginShouldThrowWhenPasswordInvalid() {
|
|
var request = new LoginRequest("sarah@example.com", "wrongpass");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(passwordEncoder.matches("wrongpass", "hashed")).thenReturn(false);
|
|
|
|
assertThatThrownBy(() -> authService.login(request))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
@Test
|
|
void loginShouldThrowWhenAccountInactive() {
|
|
var request = new LoginRequest("sarah@example.com", "s3cure!Pass");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
user.setActive(false);
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
|
|
assertThatThrownBy(() -> authService.login(request))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
@Test
|
|
void getCurrentUserShouldReturnUserWithHouseholdInfo() {
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
var household = new Household("Smith family", user);
|
|
var member = new HouseholdMember(household, user, "planner");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(householdMemberRepository.findByUserEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(member));
|
|
|
|
UserResponse result = authService.getCurrentUser("sarah@example.com");
|
|
|
|
assertThat(result.email()).isEqualTo("sarah@example.com");
|
|
assertThat(result.householdName()).isEqualTo("Smith family");
|
|
assertThat(result.householdRole()).isEqualTo("planner");
|
|
}
|
|
|
|
@Test
|
|
void updateProfileShouldUpdateDisplayName() {
|
|
var request = new UpdateProfileRequest("Sarah S.", null, null);
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(i -> i.getArgument(0));
|
|
|
|
UserResponse result = authService.updateProfile("sarah@example.com", request);
|
|
|
|
assertThat(result.displayName()).isEqualTo("Sarah S.");
|
|
}
|
|
|
|
@Test
|
|
void updateProfileShouldChangePasswordWhenCurrentPasswordValid() {
|
|
var request = new UpdateProfileRequest(null, "oldpass", "newpassword");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed_old");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(passwordEncoder.matches("oldpass", "hashed_old")).thenReturn(true);
|
|
when(passwordEncoder.encode("newpassword")).thenReturn("hashed_new");
|
|
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(i -> i.getArgument(0));
|
|
|
|
authService.updateProfile("sarah@example.com", request);
|
|
|
|
verify(passwordEncoder).encode("newpassword");
|
|
}
|
|
|
|
@Test
|
|
void updateProfileShouldThrowWhenCurrentPasswordWrong() {
|
|
var request = new UpdateProfileRequest(null, "wrongpass", "newpassword");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed_old");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
when(passwordEncoder.matches("wrongpass", "hashed_old")).thenReturn(false);
|
|
|
|
assertThatThrownBy(() -> authService.updateProfile("sarah@example.com", request))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
@Test
|
|
void updateProfileShouldThrowWhenNewPasswordWithoutCurrentPassword() {
|
|
var request = new UpdateProfileRequest(null, null, "newpassword");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
|
|
when(userAccountRepository.findByEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(user));
|
|
|
|
assertThatThrownBy(() -> authService.updateProfile("sarah@example.com", request))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
}
|