Files
mealprep/backend/src/test/java/com/recipeapp/auth/AuthServiceTest.java
Marcel Raddatz 3253dcfec2 Implement auth domain with outside-in TDD (22 tests)
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>
2026-04-01 21:24:26 +02:00

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);
}
}