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>
This commit is contained in:
2026-04-01 21:24:26 +02:00
parent 866603711d
commit 3253dcfec2
23 changed files with 873 additions and 15 deletions

View File

@@ -0,0 +1,43 @@
package com.recipeapp.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v1/auth/signup", "/v1/auth/login").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/v1/admin/**").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.sessionManagement(session -> session
.maximumSessions(1));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}