diff --git a/server/build.gradle b/server/build.gradle index 526c3640e..9e59b348f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -28,10 +28,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/server/src/main/java/server/haengdong/application/AuthService.java b/server/src/main/java/server/haengdong/application/AuthService.java new file mode 100644 index 000000000..6a22fcbe2 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/AuthService.java @@ -0,0 +1,40 @@ +package server.haengdong.application; + + +import java.util.Map; +import server.haengdong.domain.TokenProvider; +import server.haengdong.exception.AuthenticationException; + +public class AuthService { + + private static final String TOKEN_NAME = "eventToken"; + private static final String CLAIM_SUB = "sub"; + + private final TokenProvider tokenProvider; + + public AuthService(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + public String createToken(String eventId) { + Map payload = Map.of(CLAIM_SUB, eventId); + + return tokenProvider.createToken(payload); + } + + public String findEventIdByToken(String token) { + validateToken(token); + Map payload = tokenProvider.getPayload(token); + return (String) payload.get(CLAIM_SUB); + } + + private void validateToken(String token) { + if (!tokenProvider.validateToken(token)) { + throw new AuthenticationException(); + } + } + + public String getTokenName() { + return TOKEN_NAME; + } +} diff --git a/server/src/main/java/server/haengdong/application/EventService.java b/server/src/main/java/server/haengdong/application/EventService.java index 52080ae8f..01f198571 100644 --- a/server/src/main/java/server/haengdong/application/EventService.java +++ b/server/src/main/java/server/haengdong/application/EventService.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import server.haengdong.application.request.EventAppRequest; +import server.haengdong.application.request.EventLoginAppRequest; import server.haengdong.application.request.MemberUpdateAppRequest; import server.haengdong.application.response.ActionAppResponse; import server.haengdong.application.response.EventAppResponse; @@ -19,6 +20,7 @@ import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.domain.event.EventTokenProvider; +import server.haengdong.exception.AuthenticationException; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; @@ -42,15 +44,13 @@ public EventAppResponse saveEvent(EventAppRequest request) { } public EventDetailAppResponse findEvent(String token) { - Event event = eventRepository.findByToken(token) - .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + Event event = getEvent(token); return EventDetailAppResponse.of(event); } public List findActions(String token) { - Event event = eventRepository.findByToken(token) - .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + Event event = getEvent(token); List billActions = billActionRepository.findByAction_Event(event).stream() .sorted(Comparator.comparing(BillAction::getSequence)).toList(); @@ -92,8 +92,7 @@ private List getActionAppResponses( } public MembersAppResponse findAllMembers(String token) { - Event event = eventRepository.findByToken(token) - .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + Event event = getEvent(token); List memberNames = memberActionRepository.findAllUniqueMemberByEvent(event); @@ -102,8 +101,7 @@ public MembersAppResponse findAllMembers(String token) { @Transactional public void updateMember(String token, String memberName, MemberUpdateAppRequest request) { - Event event = eventRepository.findByToken(token) - .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + Event event = getEvent(token); String updatedMemberName = request.name(); validateMemberNameUnique(event, updatedMemberName); @@ -117,4 +115,16 @@ private void validateMemberNameUnique(Event event, String updatedMemberName) { throw new HaengdongException(HaengdongErrorCode.DUPLICATED_MEMBER_NAME); } } + + public void validatePassword(EventLoginAppRequest request) throws HaengdongException { + Event event = getEvent(request.token()); + if (event.isSamePassword(request.password())) { + throw new AuthenticationException(); + } + } + + private Event getEvent(String token) { + return eventRepository.findByToken(token) + .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + } } diff --git a/server/src/main/java/server/haengdong/application/request/EventAppRequest.java b/server/src/main/java/server/haengdong/application/request/EventAppRequest.java index 1eea6adf4..20ec16d88 100644 --- a/server/src/main/java/server/haengdong/application/request/EventAppRequest.java +++ b/server/src/main/java/server/haengdong/application/request/EventAppRequest.java @@ -2,9 +2,9 @@ import server.haengdong.domain.event.Event; -public record EventAppRequest(String name) { +public record EventAppRequest(String name, String password) { public Event toEvent(String token) { - return new Event(name, token); + return new Event(name, password, token); } } diff --git a/server/src/main/java/server/haengdong/application/request/EventLoginAppRequest.java b/server/src/main/java/server/haengdong/application/request/EventLoginAppRequest.java new file mode 100644 index 000000000..947b5ef39 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/request/EventLoginAppRequest.java @@ -0,0 +1,4 @@ +package server.haengdong.application.request; + +public record EventLoginAppRequest(String token, String password) { +} diff --git a/server/src/main/java/server/haengdong/config/AdminInterceptor.java b/server/src/main/java/server/haengdong/config/AdminInterceptor.java new file mode 100644 index 000000000..e8b9793ed --- /dev/null +++ b/server/src/main/java/server/haengdong/config/AdminInterceptor.java @@ -0,0 +1,44 @@ +package server.haengdong.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; +import server.haengdong.application.AuthService; +import server.haengdong.exception.AuthenticationException; +import server.haengdong.infrastructure.auth.AuthenticationExtractor; + +@Slf4j +public class AdminInterceptor implements HandlerInterceptor { + + private final AuthService authService; + private final AuthenticationExtractor authenticationExtractor; + + public AdminInterceptor(AuthService authService, AuthenticationExtractor authenticationExtractor) { + this.authService = authService; + this.authenticationExtractor = authenticationExtractor; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + log.trace("login request = {}", request.getRequestURI()); + + String method = request.getMethod(); + if (method.equals("GET")) { + return true; + } + + validateToken(request); + + return true; + } + + private void validateToken(HttpServletRequest request) { + String token = authenticationExtractor.extract(request, authService.getTokenName()); + String tokenEventId = authService.findEventIdByToken(token); + String eventId = request.getRequestURI().split("/")[2]; + if (!tokenEventId.equals(eventId)) { + throw new AuthenticationException(); + } + } +} diff --git a/server/src/main/java/server/haengdong/config/WebConfig.java b/server/src/main/java/server/haengdong/config/WebConfig.java deleted file mode 100644 index 129d8b790..000000000 --- a/server/src/main/java/server/haengdong/config/WebConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package server.haengdong.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@RequiredArgsConstructor -@Configuration -public class WebConfig implements WebMvcConfigurer { - - @Value("${cors.max-age}") - private Long maxAge; - - @Value("${cors.allowed-origins}") - private String[] allowedOrigins; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(allowedOrigins) - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .maxAge(maxAge); - } -} diff --git a/server/src/main/java/server/haengdong/config/WebMvcConfig.java b/server/src/main/java/server/haengdong/config/WebMvcConfig.java new file mode 100644 index 000000000..3d2d9f60b --- /dev/null +++ b/server/src/main/java/server/haengdong/config/WebMvcConfig.java @@ -0,0 +1,65 @@ +package server.haengdong.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import server.haengdong.application.AuthService; +import server.haengdong.domain.TokenProvider; +import server.haengdong.infrastructure.auth.AuthenticationExtractor; +import server.haengdong.infrastructure.auth.JwtTokenProvider; +import server.haengdong.infrastructure.auth.TokenProperties; + +@RequiredArgsConstructor +@EnableConfigurationProperties(TokenProperties.class) +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final TokenProperties tokenProperties; + + @Value("${cors.max-age}") + private Long maxAge; + + @Value("${cors.allowed-origins}") + private String[] allowedOrigins; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins) + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .maxAge(maxAge); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminInterceptor()) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/events"); + } + + @Bean + public AdminInterceptor adminInterceptor() { + return new AdminInterceptor(authService(), authenticationExtractor()); + } + + @Bean + public AuthService authService() { + return new AuthService(tokenProvider()); + } + + @Bean + public TokenProvider tokenProvider() { + return new JwtTokenProvider(tokenProperties); + } + + @Bean + public AuthenticationExtractor authenticationExtractor() { + return new AuthenticationExtractor(); + } +} diff --git a/server/src/main/java/server/haengdong/domain/TokenProvider.java b/server/src/main/java/server/haengdong/domain/TokenProvider.java new file mode 100644 index 000000000..28e7956c3 --- /dev/null +++ b/server/src/main/java/server/haengdong/domain/TokenProvider.java @@ -0,0 +1,12 @@ +package server.haengdong.domain; + +import java.util.Map; + +public interface TokenProvider { + + String createToken(Map payload); + + Map getPayload(String token); + + boolean validateToken(String token); +} diff --git a/server/src/main/java/server/haengdong/domain/event/Event.java b/server/src/main/java/server/haengdong/domain/event/Event.java index 17bffa44d..f43145f0d 100644 --- a/server/src/main/java/server/haengdong/domain/event/Event.java +++ b/server/src/main/java/server/haengdong/domain/event/Event.java @@ -4,6 +4,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,6 +20,7 @@ public class Event { private static final int MIN_NAME_LENGTH = 2; private static final int MAX_NAME_LENGTH = 20; private static final String SPACES = " "; + private static final Pattern PASSWORD_PATTERN = Pattern.compile("^\\d{4}$"); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,11 +28,15 @@ public class Event { private String name; + private String password; + private String token; - public Event(String name, String token) { + public Event(String name, String password, String token) { validateName(name); + validatePassword(password); this.name = name; + this.password = password; this.token = token; } @@ -48,6 +55,13 @@ private void validateName(String name) { } } + private void validatePassword(String password) { + Matcher matcher = PASSWORD_PATTERN.matcher(password); + if (!matcher.matches()) { + throw new HaengdongException(HaengdongErrorCode.BAD_REQUEST, "비밀번호는 4자리 숫자만 가능합니다."); + } + } + private boolean isBlankContinuous(String name) { return name.contains(SPACES); } @@ -55,4 +69,8 @@ private boolean isBlankContinuous(String name) { public boolean isTokenMismatch(String token) { return !this.token.equals(token); } + + public boolean isSamePassword(String password) { + return this.password.equals(password); + } } diff --git a/server/src/main/java/server/haengdong/exception/AuthenticationException.java b/server/src/main/java/server/haengdong/exception/AuthenticationException.java new file mode 100644 index 000000000..78a16e295 --- /dev/null +++ b/server/src/main/java/server/haengdong/exception/AuthenticationException.java @@ -0,0 +1,4 @@ +package server.haengdong.exception; + +public class AuthenticationException extends RuntimeException { +} diff --git a/server/src/main/java/server/haengdong/exception/GlobalExceptionHandler.java b/server/src/main/java/server/haengdong/exception/GlobalExceptionHandler.java index fb15afc56..e45f2da67 100644 --- a/server/src/main/java/server/haengdong/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/server/haengdong/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -14,6 +15,12 @@ @RestControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity authenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ErrorResponse.of(HaengdongErrorCode.UNAUTHORIZED)); + } + @ExceptionHandler({HttpRequestMethodNotSupportedException.class, NoResourceFoundException.class}) public ResponseEntity noResourceException() { return ResponseEntity.badRequest() diff --git a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java index c9f461c5f..93667d052 100644 --- a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java +++ b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java @@ -7,6 +7,7 @@ public enum HaengdongErrorCode { BAD_REQUEST("R_001", "잘못된 요청입니다."), NO_RESOURCE_REQUEST("R_002", "잘못된 엔드포인트입니다."), MESSAGE_NOT_READABLE("R_003", "읽을 수 없는 요청 형식입니다."), + UNAUTHORIZED("A_001", "인증에 실패했습니다."), INTERNAL_SERVER_ERROR("S_001", "서버 내부에서 에러가 발생했습니다."), DUPLICATED_MEMBER_NAME("EV_001", "중복된 행사 참여 인원 이름이 존재합니다."), NOT_FOUND_EVENT("EV_400", "존재하지 않는 행사입니다."), diff --git a/server/src/main/java/server/haengdong/infrastructure/auth/AuthenticationExtractor.java b/server/src/main/java/server/haengdong/infrastructure/auth/AuthenticationExtractor.java new file mode 100644 index 000000000..74489577d --- /dev/null +++ b/server/src/main/java/server/haengdong/infrastructure/auth/AuthenticationExtractor.java @@ -0,0 +1,22 @@ +package server.haengdong.infrastructure.auth; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import server.haengdong.exception.AuthenticationException; + +public class AuthenticationExtractor { + + public String extract(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new AuthenticationException(); + } + + return Arrays.stream(cookies) + .filter(cookie -> cookieName.equals(cookie.getName())) + .findFirst() + .orElseThrow(AuthenticationException::new) + .getValue(); + } +} diff --git a/server/src/main/java/server/haengdong/infrastructure/auth/CookieProperties.java b/server/src/main/java/server/haengdong/infrastructure/auth/CookieProperties.java new file mode 100644 index 000000000..40d4252fd --- /dev/null +++ b/server/src/main/java/server/haengdong/infrastructure/auth/CookieProperties.java @@ -0,0 +1,14 @@ +package server.haengdong.infrastructure.auth; + +import java.time.Duration; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("cookie") +public record CookieProperties( + boolean httpOnly, + boolean secure, + String domain, + String path, + Duration maxAge +) { +} diff --git a/server/src/main/java/server/haengdong/infrastructure/auth/JwtTokenProvider.java b/server/src/main/java/server/haengdong/infrastructure/auth/JwtTokenProvider.java new file mode 100644 index 000000000..df9ec6a81 --- /dev/null +++ b/server/src/main/java/server/haengdong/infrastructure/auth/JwtTokenProvider.java @@ -0,0 +1,54 @@ +package server.haengdong.infrastructure.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import server.haengdong.domain.TokenProvider; + +public class JwtTokenProvider implements TokenProvider { + + private final TokenProperties tokenProperties; + + public JwtTokenProvider(TokenProperties tokenProperties) { + this.tokenProperties = tokenProperties; + } + + @Override + public String createToken(Map payload) { + Claims claims = Jwts.claims(new HashMap<>(payload)); + Date now = new Date(); + Date validity = new Date(now.getTime() + tokenProperties.expireLength()); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, tokenProperties.secretKey()) + .compact(); + } + + @Override + public Map getPayload(String token) { + return Jwts.parser() + .setSigningKey(tokenProperties.secretKey()) + .parseClaimsJws(token) + .getBody(); + } + + @Override + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(tokenProperties.secretKey()).parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} + diff --git a/server/src/main/java/server/haengdong/infrastructure/auth/TokenProperties.java b/server/src/main/java/server/haengdong/infrastructure/auth/TokenProperties.java new file mode 100644 index 000000000..11dedcdbf --- /dev/null +++ b/server/src/main/java/server/haengdong/infrastructure/auth/TokenProperties.java @@ -0,0 +1,7 @@ +package server.haengdong.infrastructure.auth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("security.jwt.token") +public record TokenProperties(String secretKey, Long expireLength) { +} diff --git a/server/src/main/java/server/haengdong/presentation/EventController.java b/server/src/main/java/server/haengdong/presentation/EventController.java index b6be87911..3a6ce5b97 100644 --- a/server/src/main/java/server/haengdong/presentation/EventController.java +++ b/server/src/main/java/server/haengdong/presentation/EventController.java @@ -2,6 +2,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -9,7 +12,10 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import server.haengdong.application.AuthService; import server.haengdong.application.EventService; +import server.haengdong.infrastructure.auth.CookieProperties; +import server.haengdong.presentation.request.EventLoginRequest; import server.haengdong.presentation.request.EventSaveRequest; import server.haengdong.presentation.request.MemberUpdateRequest; import server.haengdong.presentation.response.EventDetailResponse; @@ -18,16 +24,24 @@ import server.haengdong.presentation.response.StepsResponse; @RequiredArgsConstructor +@EnableConfigurationProperties(CookieProperties.class) @RestController public class EventController { private final EventService eventService; + private final AuthService authService; + private final CookieProperties cookieProperties; @PostMapping("/api/events") public ResponseEntity saveEvent(@Valid @RequestBody EventSaveRequest request) { EventResponse eventResponse = EventResponse.of(eventService.saveEvent(request.toAppRequest())); - return ResponseEntity.ok(eventResponse); + String jwtToken = authService.createToken(eventResponse.eventId()); + + ResponseCookie responseCookie = createResponseCookie(jwtToken); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, responseCookie.toString()) + .body(eventResponse); } @GetMapping("/api/events/{eventId}") @@ -61,4 +75,28 @@ public ResponseEntity updateMember( return ResponseEntity.ok().build(); } + + @PostMapping("/api/events/{eventId}/login") + public ResponseEntity loginEvent( + @PathVariable("eventId") String token, + @Valid @RequestBody EventLoginRequest request + ) { + eventService.validatePassword(request.toAppRequest(token)); + String jwtToken = authService.createToken(token); + + ResponseCookie responseCookie = createResponseCookie(jwtToken); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, responseCookie.toString()) + .build(); + } + + private ResponseCookie createResponseCookie(String token) { + return ResponseCookie.from(authService.getTokenName(), token) + .httpOnly(cookieProperties.httpOnly()) + .secure(cookieProperties.secure()) + .domain(cookieProperties.domain()) + .path(cookieProperties.path()) + .maxAge(cookieProperties.maxAge()) + .build(); + } } diff --git a/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java b/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java new file mode 100644 index 000000000..d1e743821 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java @@ -0,0 +1,14 @@ +package server.haengdong.presentation.request; + +import jakarta.validation.constraints.NotBlank; +import server.haengdong.application.request.EventLoginAppRequest; + +public record EventLoginRequest( + + @NotBlank + String password +) { + public EventLoginAppRequest toAppRequest(String token) { + return new EventLoginAppRequest(token, password); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/request/EventSaveRequest.java b/server/src/main/java/server/haengdong/presentation/request/EventSaveRequest.java index b28572332..fb9683916 100644 --- a/server/src/main/java/server/haengdong/presentation/request/EventSaveRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/EventSaveRequest.java @@ -6,10 +6,13 @@ public record EventSaveRequest( @NotBlank - String eventName + String eventName, + + @NotBlank + String password ) { public EventAppRequest toAppRequest() { - return new EventAppRequest(eventName); + return new EventAppRequest(eventName, password); } } diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index c0099e93e..e3465c9ad 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -22,6 +22,18 @@ cors: max-age: 3600 allowed-origins: http://localhost:3000, https://haengdong.pro, https://dev.haengdong.pro, https://app.haengdong.pro +security: + jwt: + token: + secret-key: skdmeejEKJdkDjklDlkj123DKLJ3kDkeDkDKQMEOD1D90D8dE + expire-length: 604800 # 1주일 + +cookie: + http-only: true + secure: false + path: / + max-age: 7D + management: endpoints: web: @@ -30,15 +42,10 @@ management: logging: level: - root: info + root: debug org.springframework.web: debug server.haengdong: debug - file: - name: logs/spring-boot-application.log - path: logs - config: classpath:logback-spring.xml - --- spring: diff --git a/server/src/main/resources/config b/server/src/main/resources/config index c946d00b8..9129bc475 160000 --- a/server/src/main/resources/config +++ b/server/src/main/resources/config @@ -1 +1 @@ -Subproject commit c946d00b8f35ee39446f780cc2cf3983461cd38c +Subproject commit 9129bc4756c577a511f7372d6ee7dca6b1925b23 diff --git a/server/src/test/java/server/haengdong/application/ActionServiceTest.java b/server/src/test/java/server/haengdong/application/ActionServiceTest.java index 159bdd68e..ad6c60aa9 100644 --- a/server/src/test/java/server/haengdong/application/ActionServiceTest.java +++ b/server/src/test/java/server/haengdong/application/ActionServiceTest.java @@ -9,9 +9,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.domain.action.Action; import server.haengdong.domain.action.BillAction; @@ -22,11 +20,9 @@ import server.haengdong.domain.event.EventRepository; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; -import server.haengdong.support.extension.DatabaseCleanerExtension; +import server.haengdong.support.fixture.Fixture; -@ExtendWith(DatabaseCleanerExtension.class) -@SpringBootTest -class ActionServiceTest { +class ActionServiceTest extends ServiceTestSupport { @Autowired private ActionService actionService; @@ -43,8 +39,7 @@ class ActionServiceTest { @DisplayName("참여자별 정산 현황을 조회한다.") @Test void getMemberBillReports() { - String token = "tOkEn1"; - Event event = new Event("행동대장", token); + Event event = Fixture.EVENT1; Event savedEvent = eventRepository.save(event); List memberActions = List.of( new MemberAction(new Action(savedEvent, 1L), "소하", IN, 1L), @@ -60,7 +55,7 @@ void getMemberBillReports() { memberActionRepository.saveAll(memberActions); billActionRepository.saveAll(billActions); - List responses = actionService.getMemberBillReports(token); + List responses = actionService.getMemberBillReports(event.getToken()); assertThat(responses) .hasSize(3) diff --git a/server/src/test/java/server/haengdong/application/BillActionServiceTest.java b/server/src/test/java/server/haengdong/application/BillActionServiceTest.java index 76b576542..51eaa6e0b 100644 --- a/server/src/test/java/server/haengdong/application/BillActionServiceTest.java +++ b/server/src/test/java/server/haengdong/application/BillActionServiceTest.java @@ -8,23 +8,18 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import server.haengdong.application.request.BillActionAppRequest; import server.haengdong.application.request.BillActionUpdateAppRequest; import server.haengdong.domain.action.Action; -import server.haengdong.domain.action.ActionRepository; import server.haengdong.domain.action.BillAction; import server.haengdong.domain.action.BillActionRepository; import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.exception.HaengdongException; -import server.haengdong.support.extension.DatabaseCleanerExtension; +import server.haengdong.support.fixture.Fixture; -@ExtendWith(DatabaseCleanerExtension.class) -@SpringBootTest -class BillActionServiceTest { +class BillActionServiceTest extends ServiceTestSupport { @Autowired private BillActionService billActionService; @@ -38,8 +33,7 @@ class BillActionServiceTest { @DisplayName("지출 내역을 생성한다.") @Test void saveAllBillAction() { - String token = "TOKEN2"; - Event event = new Event("감자", token); + Event event = Fixture.EVENT1; Event savedEvent = eventRepository.save(event); List requests = List.of( @@ -47,7 +41,7 @@ void saveAllBillAction() { new BillActionAppRequest("인생맥주", 15_000L) ); - billActionService.saveAllBillAction(token, requests); + billActionService.saveAllBillAction(event.getToken(), requests); List actions = billActionRepository.findByAction_Event(savedEvent); @@ -73,8 +67,7 @@ void saveAllBillAction1() { @DisplayName("지출 액션을 수정한다.") @Test void updateBillAction() { - String token = "TOKEN"; - Event event = new Event("감자", token); + Event event = Fixture.EVENT1; Event savedEvent = eventRepository.save(event); Action action = Action.createFirst(savedEvent); BillAction billAction = new BillAction(action, "뽕족", 10_000L); @@ -83,7 +76,7 @@ void updateBillAction() { Long actionId = savedBillAction.getAction().getId(); BillActionUpdateAppRequest request = new BillActionUpdateAppRequest("인생맥주", 20_000L); - billActionService.updateBillAction(token, actionId, request); + billActionService.updateBillAction(event.getToken(), actionId, request); BillAction updatedBillAction = billActionRepository.findById(savedBillAction.getId()).get(); @@ -96,10 +89,8 @@ void updateBillAction() { @DisplayName("행사에 속하지 않은 지출 액션은 수정할 수 없다.") @Test void updateBillAction1() { - String token1 = "쿠키 토큰"; - String token2 = "웨디 토큰"; - Event event1 = new Event("감자1", token1); - Event event2 = new Event("감자2", token2); + Event event1 = Fixture.EVENT1; + Event event2 = Fixture.EVENT2; Event savedEvent1 = eventRepository.save(event1); Event savedEvent2 = eventRepository.save(event2); Action action1 = Action.createFirst(savedEvent1); @@ -112,21 +103,20 @@ void updateBillAction1() { Long actionId = savedBillAction1.getAction().getId(); BillActionUpdateAppRequest request = new BillActionUpdateAppRequest("인생맥주", 20_000L); - assertThatThrownBy(() -> billActionService.updateBillAction(token2, actionId, request)) + assertThatThrownBy(() -> billActionService.updateBillAction(event2.getToken(), actionId, request)) .isInstanceOf(HaengdongException.class); } @DisplayName("지출 내역을 삭제한다.") @Test void deleteBillAction() { - String token = "토다리 토큰"; - Event event = new Event("감자", token); + Event event = Fixture.EVENT1; eventRepository.save(event); BillAction billAction = new BillAction(new Action(event, 1L), "커피", 50_900L); billActionRepository.save(billAction); Long actionId = billAction.getAction().getId(); - billActionService.deleteBillAction(token, actionId); + billActionService.deleteBillAction(event.getToken(), actionId); assertThat(billActionRepository.findById(billAction.getId())).isEmpty(); } diff --git a/server/src/test/java/server/haengdong/application/EventServiceTest.java b/server/src/test/java/server/haengdong/application/EventServiceTest.java index bf85d1bc3..f5694efa1 100644 --- a/server/src/test/java/server/haengdong/application/EventServiceTest.java +++ b/server/src/test/java/server/haengdong/application/EventServiceTest.java @@ -11,9 +11,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.request.MemberUpdateAppRequest; @@ -29,12 +27,10 @@ import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.domain.event.EventTokenProvider; -import server.haengdong.support.extension.DatabaseCleanerExtension; import server.haengdong.exception.HaengdongException; +import server.haengdong.support.fixture.Fixture; -@ExtendWith(DatabaseCleanerExtension.class) -@SpringBootTest -class EventServiceTest { +class EventServiceTest extends ServiceTestSupport { @Autowired private EventService eventService; @@ -54,7 +50,7 @@ class EventServiceTest { @DisplayName("행사를 생성한다") @Test void saveEventTest() { - EventAppRequest request = new EventAppRequest("test"); + EventAppRequest request = new EventAppRequest("test", "1234"); given(eventTokenProvider.createToken()).willReturn("TOKEN"); EventAppResponse response = eventService.saveEvent(request); @@ -65,19 +61,18 @@ void saveEventTest() { @DisplayName("토큰으로 행사를 조회한다.") @Test void findEventTest() { - String token = "TOKEN"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; eventRepository.save(event); - EventDetailAppResponse eventDetailAppResponse = eventService.findEvent(token); + EventDetailAppResponse eventDetailAppResponse = eventService.findEvent(event.getToken()); - assertThat(eventDetailAppResponse.eventName()).isEqualTo("행동대장 회식"); + assertThat(eventDetailAppResponse.eventName()).isEqualTo(event.getName()); } @DisplayName("행사에 속한 모든 액션을 조회한다.") @Test void findActionsTest() { - Event event = new Event("행동대장 회식", "웨디_토큰"); + Event event = Fixture.EVENT1; Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "토다리", IN, 1L); Action action1 = new Action(event, 2L); @@ -88,7 +83,7 @@ void findActionsTest() { memberActionRepository.saveAll(List.of(memberAction, memberAction1)); billActionRepository.save(billAction); - List actionAppResponses = eventService.findActions("웨디_토큰"); + List actionAppResponses = eventService.findActions(event.getToken()); assertThat(actionAppResponses).hasSize(3) .extracting(ActionAppResponse::actionId, @@ -106,8 +101,7 @@ void findActionsTest() { @DisplayName("행사에 참여한 전체 인원을 중복 없이 조회한다.") @Test void findAllMembersTest() { - String token = "웨디_토큰"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; Action action1 = new Action(event, 1L); Action action2 = new Action(event, 2L); Action action3 = new Action(event, 3L); @@ -120,7 +114,7 @@ void findAllMembersTest() { billActionRepository.save(billAction); memberActionRepository.saveAll(List.of(memberAction1, memberAction2, memberAction3)); - MembersAppResponse membersAppResponse = eventService.findAllMembers(token); + MembersAppResponse membersAppResponse = eventService.findAllMembers(event.getToken()); assertThat(membersAppResponse.memberNames()).containsExactlyInAnyOrder("토다리", "쿠키"); } @@ -128,8 +122,7 @@ void findAllMembersTest() { @DisplayName("행사 참여 인원의 이름을 변경한다.") @Test void updateMember() { - String token = "행동대장 회식"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "토다리", IN, 1L); MemberAction memberAction2 = new MemberAction(new Action(event, 2L), "쿠키", IN, 1L); MemberAction memberAction3 = new MemberAction(new Action(event, 3L), "웨디", IN, 2L); @@ -141,7 +134,7 @@ void updateMember() { memberAction1, memberAction2, memberAction3, memberAction4, memberAction5, memberAction6 )); - eventService.updateMember(token, "쿠키", new MemberUpdateAppRequest("쿡쿡")); + eventService.updateMember(event.getToken(), "쿠키", new MemberUpdateAppRequest("쿡쿡")); List foundMemberActions = memberActionRepository.findAllByEvent(event); assertThat(foundMemberActions) @@ -159,15 +152,14 @@ void updateMember() { @DisplayName("참여 인원 이름을 이미 존재하는 행사 참여 인원과 동일한 이름으로 변경할 수 없다.") @Test void updateMember1() { - String token = "행동대장 회식"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "토다리", IN, 1L); MemberAction memberAction2 = new MemberAction(new Action(event, 2L), "쿠키", IN, 1L); MemberAction memberAction3 = new MemberAction(new Action(event, 3L), "웨디", IN, 2L); eventRepository.save(event); memberActionRepository.saveAll(List.of(memberAction1, memberAction2, memberAction3)); - assertThatThrownBy(() -> eventService.updateMember(token, "쿠키", new MemberUpdateAppRequest("토다리"))) + assertThatThrownBy(() -> eventService.updateMember(event.getToken(), "쿠키", new MemberUpdateAppRequest("토다리"))) .isInstanceOf(HaengdongException.class); } } diff --git a/server/src/test/java/server/haengdong/application/MemberActionFactoryTest.java b/server/src/test/java/server/haengdong/application/MemberActionFactoryTest.java index 2c2a978ae..c04f06026 100644 --- a/server/src/test/java/server/haengdong/application/MemberActionFactoryTest.java +++ b/server/src/test/java/server/haengdong/application/MemberActionFactoryTest.java @@ -8,9 +8,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import server.haengdong.application.request.MemberActionSaveAppRequest; import server.haengdong.application.request.MemberActionsSaveAppRequest; import server.haengdong.domain.action.Action; @@ -21,11 +19,9 @@ import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.exception.HaengdongException; -import server.haengdong.support.extension.DatabaseCleanerExtension; +import server.haengdong.support.fixture.Fixture; -@ExtendWith(DatabaseCleanerExtension.class) -@SpringBootTest -class MemberActionFactoryTest { +class MemberActionFactoryTest extends ServiceTestSupport { @Autowired private MemberActionFactory memberActionFactory; @@ -39,7 +35,7 @@ class MemberActionFactoryTest { @DisplayName("이전 멤버 액션이 시퀀스 기준으로 정렬되지 않은 상태에서 새로운 멤버 액션 요청을 검증한다.") @Test void createMemberActionsTest() { - Event event = eventRepository.save(new Event("우당탕탕 행동대장 백엔드 회식", "토다리_토큰")); + Event event = eventRepository.save(Fixture.EVENT1); Action action1 = new Action(event, 1L); Action action2 = new Action(event, 2L); MemberAction memberAction1 = new MemberAction(action1, "토다리", MemberActionStatus.IN, 1L); @@ -60,7 +56,7 @@ void createMemberActionsTest() { @DisplayName("인원 변동 액션을 생성한다.") @Test void createMemberActionsTest1() { - Event event = eventRepository.save(new Event("우당탕탕 행동대장 백엔드 회식", "토다리_토큰")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "토다리", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); @@ -84,7 +80,7 @@ void createMemberActionsTest1() { @DisplayName("현재 행사에 참여 중인 경우에 퇴장할 수 있다.") @Test void createMemberActionsTest2() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "토다리", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); @@ -101,7 +97,7 @@ void createMemberActionsTest2() { @DisplayName("행사에서 퇴장한 경우에 입장할 수 있다.") @Test void createMemberActionsTest3() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action1 = new Action(event, 1L); MemberAction memberAction1 = new MemberAction(action1, "토다리", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction1); @@ -121,7 +117,7 @@ void createMemberActionsTest3() { @DisplayName("행사에 입장한 적 없는 경우에 입장할 수 있다.") @Test void createMemberActionsTest4() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "토다리", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); @@ -138,7 +134,7 @@ void createMemberActionsTest4() { @DisplayName("행사에 입장하지 않았을 경우 퇴장할 수 없다.") @Test void createMemberActionTest5() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); MemberActionsSaveAppRequest request = new MemberActionsSaveAppRequest( List.of(new MemberActionSaveAppRequest("쿠키", "OUT"))); @@ -153,7 +149,7 @@ void createMemberActionTest5() { @DisplayName("행사에 이미 참여 중인 경우 다시 입장할 수 없다.") @Test void createMemberActionTest6() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "쿠키", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); @@ -170,7 +166,7 @@ void createMemberActionTest6() { @DisplayName("한 명의 사용자는 동시에 여러 번 입장할 수 없다.") @Test void createMemberActionTest7() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); MemberActionsSaveAppRequest request = new MemberActionsSaveAppRequest( List.of( @@ -188,7 +184,7 @@ void createMemberActionTest7() { @DisplayName("한 명의 사용자는 동시에 여러 번 퇴장할 수 없다.") @Test void createMemberActionTest8() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "쿠키", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); @@ -208,7 +204,7 @@ void createMemberActionTest8() { @DisplayName("한 명의 사용자는 입장과 퇴장을 동시에 할 수 없다.") @Test void createMemberActionTest9() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "쿠키", MemberActionStatus.IN, 1L); memberActionRepository.save(memberAction); diff --git a/server/src/test/java/server/haengdong/application/MemberActionServiceTest.java b/server/src/test/java/server/haengdong/application/MemberActionServiceTest.java index 5bc9fd1ea..8f0af8dfe 100644 --- a/server/src/test/java/server/haengdong/application/MemberActionServiceTest.java +++ b/server/src/test/java/server/haengdong/application/MemberActionServiceTest.java @@ -10,9 +10,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import server.haengdong.application.request.MemberActionSaveAppRequest; import server.haengdong.application.request.MemberActionsSaveAppRequest; import server.haengdong.domain.action.Action; @@ -22,11 +20,9 @@ import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.exception.HaengdongException; -import server.haengdong.support.extension.DatabaseCleanerExtension; +import server.haengdong.support.fixture.Fixture; -@ExtendWith(DatabaseCleanerExtension.class) -@SpringBootTest -class MemberActionServiceTest { +class MemberActionServiceTest extends ServiceTestSupport { @Autowired private MemberActionService memberActionService; @@ -40,12 +36,12 @@ class MemberActionServiceTest { @DisplayName("현재 행사에 참여하고 있는 경우에 나갈 수 있다.") @Test void saveMemberActionTest() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action action = new Action(event, 1L); MemberAction memberAction = new MemberAction(action, "망쵸", IN, 1L); memberActionRepository.save(memberAction); - assertThatCode(() -> memberActionService.saveMemberAction("TOKEN", new MemberActionsSaveAppRequest( + assertThatCode(() -> memberActionService.saveMemberAction(event.getToken(), new MemberActionsSaveAppRequest( List.of(new MemberActionSaveAppRequest("망쵸", "OUT"))))) .doesNotThrowAnyException(); } @@ -53,7 +49,7 @@ void saveMemberActionTest() { @DisplayName("행사에서 퇴장한 경우에 입장할 수 있다.") @Test void saveMemberActionTest1() { - Event event = eventRepository.save(new Event("test", "TOKEN")); + Event event = eventRepository.save(Fixture.EVENT1); Action actionOne = new Action(event, 1L); MemberAction memberActionOne = new MemberAction(actionOne, "망쵸", IN, 1L); memberActionRepository.save(memberActionOne); @@ -62,7 +58,7 @@ void saveMemberActionTest1() { MemberAction memberActionTwo = new MemberAction(actionTwo, "망쵸", OUT, 1L); memberActionRepository.save(memberActionTwo); - assertThatCode(() -> memberActionService.saveMemberAction("TOKEN", new MemberActionsSaveAppRequest( + assertThatCode(() -> memberActionService.saveMemberAction(event.getToken(), new MemberActionsSaveAppRequest( List.of(new MemberActionSaveAppRequest("망쵸", "IN"))))) .doesNotThrowAnyException(); } @@ -87,8 +83,7 @@ void getCurrentMembers() { @DisplayName("행사의 전체 참여자 중에서 특정 참여자의 맴버 액션을 전부 삭제한다.") @Test void deleteMember() { - String token = "TOKEN"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; eventRepository.save(event); MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "참여자", IN, 1L); MemberAction memberAction2 = new MemberAction(new Action(event, 2L), "토다리", IN, 1L); @@ -99,14 +94,13 @@ void deleteMember() { memberActionRepository.saveAll( List.of(memberAction1, memberAction2, memberAction3, memberAction4, memberAction5, memberAction6)); - String token2 = "TOKEN2"; - Event event2 = new Event("옆동네 회식", token2); + Event event2 = Fixture.EVENT2; eventRepository.save(event2); Action action2 = Action.createFirst(event2); MemberAction anotherMemberAction = new MemberAction(action2, "참여자", IN, 1L); memberActionRepository.save(anotherMemberAction); - memberActionService.deleteMember(token, "참여자"); + memberActionService.deleteMember(event.getToken(), "참여자"); List memberActions = memberActionRepository.findAll(); @@ -124,8 +118,7 @@ void deleteMember() { @DisplayName("이벤트에 속한 멤버 액션을 삭제하면 이후에 기록된 해당 참여자의 모든 멤버 액션을 삭제한다.") @Test void deleteMemberAction() { - String token = "TOKEN"; - Event event = new Event("행동대장 회식", token); + Event event = Fixture.EVENT1; eventRepository.save(event); MemberAction memberAction1 = createMemberAction(new Action(event, 1L), "토다리", IN, 1L); Action targetAction = new Action(event, 2L); @@ -145,7 +138,7 @@ void deleteMemberAction() { memberAction7) ); - memberActionService.deleteMemberAction(token, targetAction.getId()); + memberActionService.deleteMemberAction(event.getToken(), targetAction.getId()); List memberActions = memberActionRepository.findAll(); assertThat(memberActions).hasSize(4) diff --git a/server/src/test/java/server/haengdong/application/ServiceTestSupport.java b/server/src/test/java/server/haengdong/application/ServiceTestSupport.java new file mode 100644 index 000000000..b0e2db0a6 --- /dev/null +++ b/server/src/test/java/server/haengdong/application/ServiceTestSupport.java @@ -0,0 +1,11 @@ +package server.haengdong.application; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import server.haengdong.support.extension.DatabaseCleanerExtension; + +@ExtendWith(DatabaseCleanerExtension.class) +@SpringBootTest(webEnvironment= WebEnvironment.NONE) +abstract class ServiceTestSupport { +} diff --git a/server/src/test/java/server/haengdong/domain/action/ActionTest.java b/server/src/test/java/server/haengdong/domain/action/ActionTest.java index 199a4f628..6ff25479c 100644 --- a/server/src/test/java/server/haengdong/domain/action/ActionTest.java +++ b/server/src/test/java/server/haengdong/domain/action/ActionTest.java @@ -5,13 +5,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import server.haengdong.domain.event.Event; +import server.haengdong.support.fixture.Fixture; class ActionTest { @DisplayName("액션의 초기 순서번호는 1이다.") @Test void createFirst() { - Event event = new Event("name", "token"); + Event event = Fixture.EVENT1; Action action = Action.createFirst(event); assertThat(action.getSequence()).isOne(); @@ -20,7 +21,7 @@ void createFirst() { @DisplayName("현재 액션의 다음 액션의 순서는 1만큼 증가한다.") @Test void next() { - Event event = new Event("name", "token"); + Event event = Fixture.EVENT1; Action action = new Action(event, 2L); Action nextAction = action.next(); diff --git a/server/src/test/java/server/haengdong/domain/action/BillActionTest.java b/server/src/test/java/server/haengdong/domain/action/BillActionTest.java index 601ca95d7..851ed14d9 100644 --- a/server/src/test/java/server/haengdong/domain/action/BillActionTest.java +++ b/server/src/test/java/server/haengdong/domain/action/BillActionTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.ValueSource; import server.haengdong.domain.event.Event; import server.haengdong.exception.HaengdongException; +import server.haengdong.support.fixture.Fixture; class BillActionTest { @@ -17,7 +18,7 @@ class BillActionTest { @ParameterizedTest @ValueSource(strings = {" 감 ", "", " ", "1234567890123456789012345678901"}) void validateTitle(String title) { - Event event = new Event("name", "token"); + Event event = Fixture.EVENT1; Action action = new Action(event, 1L); Long price = 100L; @@ -30,7 +31,7 @@ void validateTitle(String title) { @ParameterizedTest @ValueSource(longs = {0, 10_000_001, 20_000_000}) void validatePrice(long price) { - Event event = new Event("name", "token"); + Event event = Fixture.EVENT1; Action action = new Action(event, 1L); String title = "title"; @@ -42,7 +43,7 @@ void validatePrice(long price) { @DisplayName("지출 내역을 올바르게 생성한다.") @Test void createBillAction() { - Event event = new Event("name", "token"); + Event event = Fixture.EVENT1; Action action = new Action(event, 1L); String title = "title"; Long price = 1_000L; diff --git a/server/src/test/java/server/haengdong/domain/action/CurrentMembersTest.java b/server/src/test/java/server/haengdong/domain/action/CurrentMembersTest.java index 3ed02ddc5..93148abd3 100644 --- a/server/src/test/java/server/haengdong/domain/action/CurrentMembersTest.java +++ b/server/src/test/java/server/haengdong/domain/action/CurrentMembersTest.java @@ -9,17 +9,16 @@ import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import server.haengdong.application.request.MemberActionSaveAppRequest; -import server.haengdong.application.request.MemberActionsSaveAppRequest; import server.haengdong.domain.event.Event; import server.haengdong.exception.HaengdongException; +import server.haengdong.support.fixture.Fixture; class CurrentMembersTest { @DisplayName("인원 변동 이력으로 현재 참여 인원을 계산한다.") @Test void of() { - Event event = new Event("test", "TOKEN"); + Event event = Fixture.EVENT1; List memberActions = List.of( new MemberAction(new Action(event, 1L), "망쵸", IN, 1L), new MemberAction(new Action(event, 2L), "백호", IN, 1L), @@ -37,7 +36,7 @@ void of() { @Test void addMemberAction1() { CurrentMembers currentMembers = new CurrentMembers(); - Event event = new Event("이벤트", "token"); + Event event = Fixture.EVENT1; MemberAction memberAction = new MemberAction(new Action(event, 1L), "웨디", IN, 1L); CurrentMembers addedCurrentMembers = currentMembers.addMemberAction(memberAction); @@ -50,7 +49,7 @@ void addMemberAction1() { @DisplayName("인원 변동 액션의 상태가 OUT이면 현재 인원에서 제외한다.") @Test void addMemberAction2() { - Event event = new Event("이벤트", "token"); + Event event = Fixture.EVENT1; MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "웨디", IN, 1L); CurrentMembers currentMembers = new CurrentMembers().addMemberAction(memberAction1); MemberAction memberAction2 = new MemberAction(new Action(event, 1L), "웨디", OUT, 1L); diff --git a/server/src/test/java/server/haengdong/domain/action/MemberBillReportTest.java b/server/src/test/java/server/haengdong/domain/action/MemberBillReportTest.java index 9ad27d5bc..197768a0e 100644 --- a/server/src/test/java/server/haengdong/domain/action/MemberBillReportTest.java +++ b/server/src/test/java/server/haengdong/domain/action/MemberBillReportTest.java @@ -9,14 +9,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import server.haengdong.domain.event.Event; +import server.haengdong.support.fixture.Fixture; class MemberBillReportTest { @DisplayName("액션 목록으로 참가자 정산 리포트를 생성한다.") @Test void createByActions() { - String token = "TOK2N"; - Event event = new Event("행동대장", token); + Event event = Fixture.EVENT1; List billActions = List.of( new BillAction(new Action(event, 4L), "뽕족", 60_000L), new BillAction(new Action(event, 6L), "인생맥주", 40_000L), diff --git a/server/src/test/java/server/haengdong/domain/event/EventTest.java b/server/src/test/java/server/haengdong/domain/event/EventTest.java index 98f6c7050..0d5b58519 100644 --- a/server/src/test/java/server/haengdong/domain/event/EventTest.java +++ b/server/src/test/java/server/haengdong/domain/event/EventTest.java @@ -13,7 +13,7 @@ class EventTest { @ParameterizedTest @ValueSource(strings = {"12", "12345678901234567890", "공 백", " 공백", "공백 ", " 공 백 "}) void createSuccessTest(String eventName) { - assertThatCode(() -> new Event(eventName, "TEST_TOKEN")) + assertThatCode(() -> new Event(eventName, "1234", "TEST_TOKEN")) .doesNotThrowAnyException(); } @@ -21,7 +21,7 @@ void createSuccessTest(String eventName) { @ParameterizedTest @ValueSource(strings = {" 공백", "공백 ", "공백 연속", "공 백"}) void createFailTest1(String eventName) { - assertThatCode(() -> new Event(eventName, "TEST_TOKEN")) + assertThatCode(() -> new Event(eventName, "1234", "TEST_TOKEN")) .isInstanceOf(HaengdongException.class) .hasMessage(String.format("행사 이름에는 공백 문자가 연속될 수 없습니다. 입력한 이름 : %s", eventName)); } @@ -30,8 +30,16 @@ void createFailTest1(String eventName) { @ParameterizedTest @ValueSource(strings = {"", " ", "123456789012345678901"}) void createFilTest2(String eventName) { - assertThatCode(() -> new Event(eventName, "TEST_TOKEN")) + assertThatCode(() -> new Event(eventName, "1234", "TEST_TOKEN")) .isInstanceOf(HaengdongException.class) .hasMessage(String.format("행사 이름은 2자 이상 20자 이하만 입력 가능합니다. 입력한 이름 길이 : %d", eventName.length())); } + + @DisplayName("비밀번호는 4자리 숫자 입니다.") + @ParameterizedTest + @ValueSource(strings = {"1", "12", "123", "12345", "adgd"}) + void validatePassword(String password) { + assertThatCode(() -> new Event("이름", password, "TEST_TOKEN")) + .isInstanceOf(HaengdongException.class); + } } diff --git a/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java b/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java index ce1345917..1de5889b5 100644 --- a/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java @@ -10,23 +10,11 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import server.haengdong.application.ActionService; import server.haengdong.application.response.MemberBillReportAppResponse; -@WebMvcTest(ActionController.class) -class ActionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private ActionService actionService; +class ActionControllerTest extends ControllerTestSupport { @DisplayName("참여자별 정산 현황을 조회한다.") @Test @@ -44,6 +32,5 @@ void getMemberBillReports() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.reports[0].price").value(equalTo(20_000))) .andExpect(MockMvcResultMatchers.jsonPath("$.reports[1].name").value(equalTo("토다리"))) .andExpect(MockMvcResultMatchers.jsonPath("$.reports[1].price").value(equalTo(200_000))); - } } diff --git a/server/src/test/java/server/haengdong/presentation/BillActionControllerTest.java b/server/src/test/java/server/haengdong/presentation/BillActionControllerTest.java index 901e31b6d..1c3984e7c 100644 --- a/server/src/test/java/server/haengdong/presentation/BillActionControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/BillActionControllerTest.java @@ -8,33 +8,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import server.haengdong.application.BillActionService; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; import server.haengdong.presentation.request.BillActionSaveRequest; -import server.haengdong.presentation.request.BillActionsSaveRequest; import server.haengdong.presentation.request.BillActionUpdateRequest; +import server.haengdong.presentation.request.BillActionsSaveRequest; -@WebMvcTest(BillActionController.class) -class BillActionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private BillActionService billActionService; +class BillActionControllerTest extends ControllerTestSupport { @DisplayName("지출 내역을 생성한다.") @Test diff --git a/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java b/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java new file mode 100644 index 000000000..a9897d9f0 --- /dev/null +++ b/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java @@ -0,0 +1,47 @@ +package server.haengdong.presentation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import server.haengdong.application.ActionService; +import server.haengdong.application.AuthService; +import server.haengdong.application.BillActionService; +import server.haengdong.application.EventService; +import server.haengdong.application.MemberActionService; + +@WebMvcTest( + controllers = { + EventController.class, + ActionController.class, + BillActionController.class, + MemberActionController.class + }, + excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {WebMvcConfigurer.class})} +) +abstract class ControllerTestSupport { + + @Autowired + protected MockMvc mockMvc; + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected EventService eventService; + + @MockBean + protected AuthService authService; + + @MockBean + protected ActionService actionService; + + @MockBean + protected MemberActionService memberActionService; + + @MockBean + protected BillActionService billActionService; +} diff --git a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java index 347b734bb..4ba7bc4a9 100644 --- a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java @@ -7,52 +7,42 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import server.haengdong.application.EventService; import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; import server.haengdong.application.response.MembersAppResponse; +import server.haengdong.presentation.request.EventLoginRequest; import server.haengdong.presentation.request.EventSaveRequest; import server.haengdong.presentation.request.MemberUpdateRequest; -@WebMvcTest(EventController.class) -class EventControllerTest { - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private EventService eventService; +class EventControllerTest extends ControllerTestSupport { @DisplayName("이벤트를 생성한다.") @Test void saveEvent() throws Exception { - EventSaveRequest eventSaveRequest = new EventSaveRequest("토다리"); + EventSaveRequest eventSaveRequest = new EventSaveRequest("토다리", "0987"); String requestBody = objectMapper.writeValueAsString(eventSaveRequest); String eventId = "망쵸토큰"; EventAppResponse eventAppResponse = new EventAppResponse(eventId); given(eventService.saveEvent(any(EventAppRequest.class))).willReturn(eventAppResponse); + given(authService.createToken(eventId)).willReturn("jwtToken"); + given(authService.getTokenName()).willReturn("eventToken"); mockMvc.perform(post("/api/events") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andDo(print()) .andExpect(status().isOk()) + .andExpect(cookie().value("eventToken", "jwtToken")) .andExpect(jsonPath("$.eventId").value("망쵸토큰")); } @@ -96,4 +86,21 @@ void updateMember() throws Exception { .andDo(print()) .andExpect(status().isOk()); } + + @DisplayName("행사 어드민이 로그인한다.") + @Test + void loginEvent() throws Exception { + String token = "TOKEN"; + EventLoginRequest eventLoginRequest = new EventLoginRequest("1234"); + String requestBody = objectMapper.writeValueAsString(eventLoginRequest); + given(authService.createToken(token)).willReturn("jwtToken"); + given(authService.getTokenName()).willReturn("eventToken"); + + mockMvc.perform(post("/api/events/{eventId}/login", token) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(cookie().value("eventToken", "jwtToken")) + .andExpect(status().isOk()); + } } diff --git a/server/src/test/java/server/haengdong/presentation/MemberActionControllerTest.java b/server/src/test/java/server/haengdong/presentation/MemberActionControllerTest.java index a195311d1..1e8fa239f 100644 --- a/server/src/test/java/server/haengdong/presentation/MemberActionControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/MemberActionControllerTest.java @@ -10,30 +10,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import server.haengdong.application.MemberActionService; import server.haengdong.application.response.CurrentMemberAppResponse; import server.haengdong.presentation.request.MemberActionsSaveRequest; -@WebMvcTest(MemberActionController.class) -class MemberActionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private MemberActionService memberActionService; +class MemberActionControllerTest extends ControllerTestSupport { @DisplayName("참여자 행동을 추가한다.") @Test diff --git a/server/src/test/java/server/haengdong/support/fixture/Fixture.java b/server/src/test/java/server/haengdong/support/fixture/Fixture.java new file mode 100644 index 000000000..eb30d8d5c --- /dev/null +++ b/server/src/test/java/server/haengdong/support/fixture/Fixture.java @@ -0,0 +1,9 @@ +package server.haengdong.support.fixture; + +import server.haengdong.domain.event.Event; + +public class Fixture { + + public static final Event EVENT1 = new Event("쿠키", "1234", "TOKEN1"); + public static final Event EVENT2 = new Event("웨디", "1234", "TOKEN2"); +} diff --git a/server/src/test/resources/application.yml b/server/src/test/resources/application.yml deleted file mode 100644 index 2d02b2712..000000000 --- a/server/src/test/resources/application.yml +++ /dev/null @@ -1,18 +0,0 @@ -spring: - h2: - console: - enabled: true - path: /h2-console - datasource: - url: jdbc:h2:mem:database - jpa: - defer-datasource-initialization: true - show-sql: true - properties: - hibernate: - format_sql: true - hibernate: - ddl-auto: create-drop -cors: - max-age: 3600 - allowed-origins: http://localhost:8080