diff --git a/gradle.properties b/gradle.properties index ff5dd21b..3be9c45d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,6 +22,4 @@ sentryVersion=4.1.1 ### AWS-CLOUD ### springCloudAwsVersion=3.1.0 ### JWT ### -jwtVersion=0.9.1 -jwtBindingVersion=4.0.1 -jwtJaxbApiVersion=2.3.1 +jwtVersion=0.11.5 diff --git a/gradle/devtool.gradle b/gradle/devtool.gradle index 5b4ad617..0f7f1cc7 100644 --- a/gradle/devtool.gradle +++ b/gradle/devtool.gradle @@ -3,13 +3,9 @@ allprojects { compileOnly "org.projectlombok:lombok:${lombokVersion}" annotationProcessor "org.projectlombok:lombok" - implementation "io.jsonwebtoken:jjwt:${jwtVersion}" - - // com.sun.xml.bind - implementation "com.sun.xml.bind:jaxb-impl:${jwtBindingVersion}" - implementation "com.sun.xml.bind:jaxb-core:${jwtBindingVersion}" - // javax.xml.bind - implementation "javax.xml.bind:jaxb-api:${jwtJaxbApiVersion}" + implementation "io.jsonwebtoken:jjwt-api:${jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}" testCompileOnly "org.projectlombok:lombok:${lombokVersion}" testAnnotationProcessor "org.projectlombok:lombok" diff --git a/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java b/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java index 782080be..12900600 100644 --- a/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java +++ b/src/main/java/net/teumteum/auth/controller/OAuthLoginController.java @@ -9,8 +9,10 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; @Slf4j +@RestController @RequiredArgsConstructor public class OAuthLoginController { @@ -18,9 +20,15 @@ public class OAuthLoginController { @GetMapping("/logins/callbacks/{provider}") @ResponseStatus(HttpStatus.OK) - public TokenResponse oAuthLogin( - @PathVariable String provider, - @RequestParam String code) { - return oAuthService.oAuthLogin(provider, code); + public TokenResponse oAuthLogin(@PathVariable String provider, + @RequestParam String code, + @RequestParam String state) { + return oAuthService.oAuthLogin(provider, code, state); + } + + @GetMapping("/favicon.ico") + @ResponseStatus(HttpStatus.OK) + public Void favicon() { + return null; } } diff --git a/src/main/java/net/teumteum/auth/domain/KakaoOAuthUserInfo.java b/src/main/java/net/teumteum/auth/domain/KakaoOAuthUserInfo.java index 497e1712..ddcdf915 100644 --- a/src/main/java/net/teumteum/auth/domain/KakaoOAuthUserInfo.java +++ b/src/main/java/net/teumteum/auth/domain/KakaoOAuthUserInfo.java @@ -10,6 +10,6 @@ public KakaoOAuthUserInfo(Map attributes) { @Override public String getOAuthId() { - return (String) attributes.get("id"); + return String.valueOf(attributes.get("id")); } } diff --git a/src/main/java/net/teumteum/auth/domain/OAuthToken.java b/src/main/java/net/teumteum/auth/domain/OAuthToken.java index fc856490..0712cd4e 100644 --- a/src/main/java/net/teumteum/auth/domain/OAuthToken.java +++ b/src/main/java/net/teumteum/auth/domain/OAuthToken.java @@ -4,29 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; public record OAuthToken( - @JsonProperty("token_type") - String tokenType, @JsonProperty("access_token") String accessToken, String scope, - - @JsonProperty("expires_in") - Integer expiresIn + @JsonProperty("token_type") + String tokenType ) { - public String getTokenType() { - return this.tokenType; - } - - public String getAccessToken() { - return this.accessToken; - } - - public String getScope() { - return this.scope; - } - - public Integer getExpiresIn() { - return this.expiresIn; - } } diff --git a/src/main/java/net/teumteum/auth/service/AuthService.java b/src/main/java/net/teumteum/auth/service/AuthService.java index 13326e5b..ee91cce1 100644 --- a/src/main/java/net/teumteum/auth/service/AuthService.java +++ b/src/main/java/net/teumteum/auth/service/AuthService.java @@ -1,7 +1,6 @@ package net.teumteum.auth.service; import jakarta.servlet.http.HttpServletRequest; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.teumteum.auth.domain.response.TokenResponse; @@ -25,16 +24,15 @@ public TokenResponse reissue(HttpServletRequest request) { String accessToken = jwtService.extractAccessToken(request); checkRefreshTokenValidation(refreshToken); - - User user = findUserByAccessToken(accessToken).orElseThrow( - () -> new IllegalArgumentException("access token 에 해당하는 user를 찾을 수 없습니다.")); + User user = findUserByAccessToken(accessToken); checkRefreshTokenMatch(user, refreshToken); return issueNewToken(user); } - public Optional findUserByAccessToken(String accessToken) { - return userConnector.findUserById(Long.parseLong(jwtService.getUserIdFromToken(accessToken))); + public User findUserByAccessToken(String accessToken) { + return userConnector.findUserById(jwtService.getUserIdFromToken(accessToken)) + .orElseThrow(() -> new IllegalArgumentException("access token 에 해당하는 user를 찾을 수 없습니다.")); } private void checkRefreshTokenValidation(String refreshToken) { diff --git a/src/main/java/net/teumteum/auth/service/OAuthService.java b/src/main/java/net/teumteum/auth/service/OAuthService.java index 2adf5f44..17529651 100644 --- a/src/main/java/net/teumteum/auth/service/OAuthService.java +++ b/src/main/java/net/teumteum/auth/service/OAuthService.java @@ -5,6 +5,7 @@ import static net.teumteum.core.security.Authenticated.카카오; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; +import java.net.URLEncoder; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -41,12 +42,12 @@ public class OAuthService { private final UserConnector userConnector; - public TokenResponse oAuthLogin(String registrationId, String code) { + public TokenResponse oAuthLogin(String registrationId, String code, String state) { ClientRegistration clientRegistration = inMemoryClientRegistrationRepository.findByRegistrationId( registrationId); Authenticated authenticated = getAuthenticated(clientRegistration.getRegistrationId()); - OAuthUserInfo oAuthUserInfo = getOAuthUserInfo(clientRegistration, authenticated, code); - return checkUserAndMakeResponse(oAuthUserInfo, authenticated); + OAuthUserInfo oAuthUserInfo = getOAuthUserInfo(clientRegistration, authenticated, code, state); + return makeResponse(oAuthUserInfo, authenticated); } private Authenticated getAuthenticated(String registrationId) { @@ -57,15 +58,16 @@ private Authenticated getAuthenticated(String registrationId) { } private OAuthUserInfo getOAuthUserInfo(ClientRegistration clientRegistration, Authenticated authenticated, - String code) { - Map oAuthAttribute = getOAuthAttribute(clientRegistration, getToken(clientRegistration, code)); + String code, String state) { + Map oAuthAttribute = getOAuthAttribute(clientRegistration, + getToken(clientRegistration, code, state)); if (authenticated == 네이버) { return new NaverOAuthUserInfo(oAuthAttribute); } return new KakaoOAuthUserInfo(oAuthAttribute); } - private TokenResponse checkUserAndMakeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) { + private TokenResponse makeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) { String oauthId = oAuthUserInfo.getOAuthId(); return getUser(oauthId, authenticated) @@ -73,31 +75,39 @@ private TokenResponse checkUserAndMakeResponse(OAuthUserInfo oAuthUserInfo, Auth .orElseGet(() -> new TokenResponse(oauthId)); } + private OAuthToken getToken(ClientRegistration clientRegistration, String code, String state) { + return WebClient.create().post() + .uri(clientRegistration.getProviderDetails().getTokenUri()) + .headers(header -> { + header.setContentType(APPLICATION_FORM_URLENCODED); + header.setAcceptCharset(Collections.singletonList(UTF_8)); + }).bodyValue(tokenRequest(clientRegistration, code, state)) + .retrieve() + .bodyToMono(OAuthToken.class).block(); + } + private Map getOAuthAttribute(ClientRegistration clientRegistration, OAuthToken oAuthToken) { - return WebClient.create().get().uri(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()) - .headers(header -> header.setBearerAuth(oAuthToken.getAccessToken())).retrieve() + return WebClient.create().get() + .uri(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()) + .headers(header -> header.setBearerAuth(oAuthToken.accessToken())).retrieve() .bodyToMono(new ParameterizedTypeReference>() { }).block(); } - private OAuthToken getToken(ClientRegistration clientRegistration, String code) { - return WebClient.create().post().uri(clientRegistration.getProviderDetails().getTokenUri()).headers(header -> { - header.setContentType(APPLICATION_FORM_URLENCODED); - header.setAcceptCharset(Collections.singletonList(UTF_8)); - }).bodyValue(tokenRequest(clientRegistration, code)).retrieve().bodyToMono(OAuthToken.class).block(); - } private Optional getUser(String oauthId, Authenticated authenticated) { return this.userConnector.findByAuthenticatedAndOAuthId(authenticated, oauthId); } - private MultiValueMap tokenRequest(ClientRegistration clientRegistration, String code) { + private MultiValueMap tokenRequest(ClientRegistration clientRegistration, String code, + String state) { MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("code", code); - formData.add("grant_type", "authorization_code"); + formData.add("grant_type", clientRegistration.getAuthorizationGrantType().getValue()); formData.add("redirect_uri", clientRegistration.getRedirectUri()); formData.add("client_secret", clientRegistration.getClientSecret()); formData.add("client_id", clientRegistration.getClientId()); + formData.add("state", URLEncoder.encode(state, UTF_8)); return formData; } } diff --git a/src/main/java/net/teumteum/core/config/RedisConfig.java b/src/main/java/net/teumteum/core/config/RedisConfig.java index 11c903c4..a5cd9ea2 100644 --- a/src/main/java/net/teumteum/core/config/RedisConfig.java +++ b/src/main/java/net/teumteum/core/config/RedisConfig.java @@ -23,11 +23,12 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); return redisTemplate; } } diff --git a/src/main/java/net/teumteum/core/security/SecurityConfig.java b/src/main/java/net/teumteum/core/security/SecurityConfig.java index fc437297..17d77fe6 100644 --- a/src/main/java/net/teumteum/core/security/SecurityConfig.java +++ b/src/main/java/net/teumteum/core/security/SecurityConfig.java @@ -28,8 +28,8 @@ @RequiredArgsConstructor public class SecurityConfig { - private static final String[] PATTERNS = {"/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**", - "/logins/**"}; + private static final String[] PATTERNS = {"/css/**", "/images/**", "/js/**", "/favicon.ico/**", "/h2-console/**", + "/logins/**", "/auth/**"}; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler accessDeniedHandler; @@ -38,14 +38,18 @@ public class SecurityConfig { @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring() - .requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico", "/error"); + .requestMatchers("/css/**", "/js/**", + "/favicon.ico", "/resources/**" + + ); } + @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable).cors(cors -> cors.configurationSource(corsConfigurationSource())) - .authorizeHttpRequests(request -> request.requestMatchers("/auth/**", "/logins/**").permitAll() - .requestMatchers(HttpMethod.POST, "/users/registers").permitAll().requestMatchers(PATTERNS).permitAll() + .authorizeHttpRequests(request -> request.requestMatchers(PATTERNS).permitAll() + .requestMatchers(HttpMethod.POST, "/users/registers").permitAll() .anyRequest().authenticated()).httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS)) @@ -61,7 +65,6 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:3000"); - config.addAllowedOrigin("https://api.teum.org"); config.addAllowedHeader("*"); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.addExposedHeader("Authorization"); diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAccessDeniedHandler.java b/src/main/java/net/teumteum/core/security/filter/JwtAccessDeniedHandler.java index 3a1f71e9..7a6778da 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAccessDeniedHandler.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAccessDeniedHandler.java @@ -1,31 +1,40 @@ package net.teumteum.core.security.filter; -import jakarta.servlet.ServletException; +import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; + +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.error.ErrorResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; -import java.io.IOException; - -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; - @Slf4j @Component +@RequiredArgsConstructor public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + @Override public void handle(HttpServletRequest request, - HttpServletResponse response, - AccessDeniedException accessDeniedException - ) throws IOException, ServletException { + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { this.sendUnAuthorizedError(response, accessDeniedException); } private void sendUnAuthorizedError(HttpServletResponse response, - Exception exception) throws IOException { + Exception exception) throws IOException { + response.setStatus(SC_FORBIDDEN); + OutputStream os = response.getOutputStream(); log.error("Responding with unauthorized error. Message - {}", exception.getMessage()); - response.sendError(SC_FORBIDDEN, exception.getMessage()); + objectMapper.writeValue(os, ErrorResponse.of("인가 과정에서 오류가 발생했습니다.")); + os.flush(); } } diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java index 9bf74010..96c4b543 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationEntryPoint.java @@ -1,10 +1,13 @@ package net.teumteum.core.security.filter; +import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.teumteum.core.error.ErrorResponse; import org.springframework.security.core.AuthenticationException; @@ -13,22 +16,25 @@ @Slf4j @Component +@RequiredArgsConstructor public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private static final String ATTRIBUTE_NAME = "exception"; + + private final ObjectMapper objectMapper; + @Override - public void commence(HttpServletRequest request, - HttpServletResponse response, - AuthenticationException authenticationException - ) throws IOException { - this.sendUnAuthenticatedError(response, authenticationException); + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authenticationException) throws IOException { + this.sendUnAuthenticatedError(request, response, authenticationException); } - private void sendUnAuthenticatedError(HttpServletResponse response, - Exception exception) throws IOException { + private void sendUnAuthenticatedError(HttpServletRequest request, HttpServletResponse response, Exception exception) + throws IOException { + response.setStatus(SC_UNAUTHORIZED); OutputStream os = response.getOutputStream(); - ObjectMapper objectMapper = new ObjectMapper(); log.error("Responding with unauthenticated error. Message - {}", exception.getMessage()); - objectMapper.writeValue(os, ErrorResponse.of("인증 과정에서 오류가 발생했습니다.")); + objectMapper.writeValue(os, ErrorResponse.of((String) request.getAttribute(ATTRIBUTE_NAME))); os.flush(); } } diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java index 98181cf4..db33f357 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java @@ -14,7 +14,6 @@ import net.teumteum.user.domain.User; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -25,6 +24,8 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String ATTRIBUTE_NAME = "exception"; + private final JwtService jwtService; private final AuthService authService; private final JwtProperty jwtProperty; @@ -39,7 +40,7 @@ protected void doFilterInternal(HttpServletRequest request, try { String token = this.resolveTokenFromRequest(request); - if (checkTokenExistenceAndValidation(token)) { + if (checkTokenExistenceAndValidation(request, token)) { User user = getUser(token); saveUserAuthentication(user); } @@ -50,12 +51,19 @@ protected void doFilterInternal(HttpServletRequest request, } private User getUser(String token) { - return this.authService.findUserByAccessToken(token) - .orElseThrow(() -> new UsernameNotFoundException("일치하는 회원 정보가 존재하지 않습니다.")); + return this.authService.findUserByAccessToken(token); } - private boolean checkTokenExistenceAndValidation(String token) { - return StringUtils.hasText(token) && this.jwtService.validateToken(token); + private boolean checkTokenExistenceAndValidation(HttpServletRequest request, String token) { + if (!StringUtils.hasText(token)) { + setRequestAttribute(request, "요청에 대한 JWT 정보가 존재하지 않습니다."); + return false; + } + if (!jwtService.validateToken(token)) { + setRequestAttribute(request, "요청에 대한 JWT 가 유효하지 않습니다."); + return false; + } + return true; } private void saveUserAuthentication(User user) { @@ -66,8 +74,13 @@ private void saveUserAuthentication(User user) { private String resolveTokenFromRequest(HttpServletRequest request) { String token = request.getHeader(jwtProperty.getAccess().getHeader()); if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { - return token.substring(jwtProperty.getBearer().length()).trim(); + return token.substring(7); } + setRequestAttribute(request, "요청에 대한 JWT 파싱 과정에서 문제가 발생했습니다."); return null; } + + private void setRequestAttribute(HttpServletRequest request, String name) { + request.setAttribute(ATTRIBUTE_NAME, name); + } } diff --git a/src/main/java/net/teumteum/core/security/service/JwtService.java b/src/main/java/net/teumteum/core/security/service/JwtService.java index d29371c6..fda0e80d 100644 --- a/src/main/java/net/teumteum/core/security/service/JwtService.java +++ b/src/main/java/net/teumteum/core/security/service/JwtService.java @@ -1,12 +1,15 @@ package net.teumteum.core.security.service; +import static io.jsonwebtoken.SignatureAlgorithm.HS512; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.HttpServletRequest; +import java.security.Key; import java.util.Date; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -14,6 +17,7 @@ import net.teumteum.auth.domain.response.TokenResponse; import net.teumteum.core.property.JwtProperty; import net.teumteum.user.domain.User; +import org.springframework.beans.factory.InitializingBean; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; @@ -22,10 +26,20 @@ @Slf4j @Service @RequiredArgsConstructor -public class JwtService { +public class JwtService implements InitializingBean { + + private static final String TOKEN_SUBJECT = "ACCESSTOKEN"; private final JwtProperty jwtProperty; private final RedisService redisService; + private Key key; + + @Override + public void afterPropertiesSet() { + byte[] secretKey = Decoders.BASE64.decode(jwtProperty.getSecret()); + key = Keys.hmacShaKeyFor(secretKey); + } + public String extractAccessToken(HttpServletRequest request) { String accessToken = request.getHeader(jwtProperty.getAccess().getHeader()); @@ -44,17 +58,16 @@ public String extractRefreshToken(HttpServletRequest request) { return null; } - public String getUserIdFromToken(String token) { + public Long getUserIdFromToken(String token) { try { - return Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes()) - .parseClaimsJws(token).getBody().getSubject(); + return Long.valueOf(getClaims(token).get("id", String.class)); } catch (Exception exception) { throw new JwtException("Access Token is not valid"); } } public TokenResponse createServiceToken(User users) { - String accessToken = createAccessToken(String.valueOf(users.getId())); + String accessToken = createAccessToken(users.getId().toString()); String refreshToken = createRefreshToken(); this.redisService.setDataWithExpiration(String.valueOf(users.getId()), refreshToken, @@ -63,38 +76,44 @@ public TokenResponse createServiceToken(User users) { return new TokenResponse(jwtProperty.getBearer() + " " + accessToken, refreshToken); } - public String createAccessToken(String payload) { - return this.createToken(payload, jwtProperty.getAccess().getExpiration()); + public String createAccessToken(String userId) { + return this.createToken(userId, jwtProperty.getAccess().getExpiration()); } public String createRefreshToken() { return this.createToken(UUID.randomUUID().toString(), jwtProperty.getRefresh().getExpiration()); } - private String createToken(String payload, Long tokenExpiration) { - Claims claims = Jwts.claims().setSubject(payload); - Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration); + private String createToken(String payLoad, Long tokenExpiration) { + Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration); return Jwts.builder() - .setClaims(claims) - .setIssuedAt(new Date()) + .setSubject(TOKEN_SUBJECT) + .claim("id", payLoad) + .signWith(key, HS512) .setExpiration(tokenExpiresIn) - .signWith(SignatureAlgorithm.HS512, jwtProperty.getSecret().getBytes()) .compact(); } public boolean validateToken(String token) { try { - Jws claimsJws = Jwts.parser().setSigningKey(jwtProperty.getSecret().getBytes()) - .parseClaimsJws(token); - return !claimsJws.getBody().getExpiration().before(new Date()); - } catch (ExpiredJwtException exception) { - log.warn("만료된 jwt 입니다."); - } catch (UnsupportedJwtException exception) { - log.warn("지원되지 않는 jwt 입니다."); - } catch (IllegalArgumentException exception) { - log.warn("jwt 에 오류가 존재합니다."); + Claims claims = getClaims(token); + return !claims.getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + log.error("JWT 가 만료되었습니다."); + } catch (UnsupportedJwtException e) { + log.error("지원되지 않는 JWT 입니다."); + } catch (IllegalArgumentException e) { + log.error("JWT 가 잘못되었습니다."); } return false; } + + private Claims getClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } } diff --git a/src/main/java/net/teumteum/core/security/service/RedisService.java b/src/main/java/net/teumteum/core/security/service/RedisService.java index a2e4cfcb..323263ac 100644 --- a/src/main/java/net/teumteum/core/security/service/RedisService.java +++ b/src/main/java/net/teumteum/core/security/service/RedisService.java @@ -2,37 +2,29 @@ import java.time.Duration; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.ValueOperations; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class RedisService { - private final StringRedisTemplate stringRedisTemplate; + private final RedisTemplate redisTemplate; public String getData(String key) { - ValueOperations valueOperations = getStringStringValueOperations(); - return valueOperations.get(key); + return (String) redisTemplate.opsForValue().get(key); } public void setData(String key, String value) { - ValueOperations valueOperations = getStringStringValueOperations(); - valueOperations.set(key, value); + redisTemplate.opsForValue().set(key, value); } public void setDataWithExpiration(String key, String value, Long duration) { - ValueOperations valueOperations = getStringStringValueOperations(); Duration expireDuration = Duration.ofSeconds(duration); - valueOperations.set(key, value, expireDuration); + redisTemplate.opsForValue().set(key, value, expireDuration); } public void deleteData(String key) { - this.stringRedisTemplate.delete(key); - } - - private ValueOperations getStringStringValueOperations() { - return this.stringRedisTemplate.opsForValue(); + redisTemplate.delete(key); } } diff --git a/src/main/java/net/teumteum/meeting/controller/MeetingController.java b/src/main/java/net/teumteum/meeting/controller/MeetingController.java index 34141d3b..bc087d78 100644 --- a/src/main/java/net/teumteum/meeting/controller/MeetingController.java +++ b/src/main/java/net/teumteum/meeting/controller/MeetingController.java @@ -2,6 +2,7 @@ import io.sentry.Sentry; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.error.ErrorResponse; import net.teumteum.core.security.service.SecurityService; @@ -13,11 +14,18 @@ import net.teumteum.meeting.service.MeetingService; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.util.List; - @RestController @RequiredArgsConstructor @RequestMapping("/meetings") @@ -30,8 +38,8 @@ public class MeetingController { @PostMapping() @ResponseStatus(HttpStatus.CREATED) public MeetingResponse createMeeting( - @RequestPart @Valid CreateMeetingRequest meetingRequest, - @RequestPart List images) { + @RequestPart @Valid CreateMeetingRequest meetingRequest, + @RequestPart List images) { Long userId = securityService.getCurrentUserId(); return meetingService.createMeeting(images, meetingRequest, userId); } @@ -45,14 +53,22 @@ public MeetingResponse getMeetingById(@PathVariable("meetingId") Long meetingId) @GetMapping @ResponseStatus(HttpStatus.OK) public PageDto getMeetingsOrderByDate( - Pageable pageable, - @RequestParam(value = "isOpen") boolean isOpen, - @RequestParam(value = "topic", required = false) Topic topic, - @RequestParam(value = "meetingAreaStreet", required = false) String meetingAreaStreet, - @RequestParam(value = "participantUserId", required = false) Long participantUserId, - @RequestParam(value = "searchWord", required = false) String searchWord) { + Pageable pageable, + @RequestParam(value = "isOpen") boolean isOpen, + @RequestParam(value = "topic", required = false) Topic topic, + @RequestParam(value = "meetingAreaStreet", required = false) String meetingAreaStreet, + @RequestParam(value = "participantUserId", required = false) Long participantUserId, + @RequestParam(value = "searchWord", required = false) String searchWord) { - return meetingService.getMeetingsBySpecification(pageable, topic, meetingAreaStreet, participantUserId, searchWord, isOpen); + return meetingService.getMeetingsBySpecification(pageable, topic, meetingAreaStreet, participantUserId, + searchWord, isOpen); + } + + @DeleteMapping("/{meetingId}") + @ResponseStatus(HttpStatus.OK) + public void deleteMeeting(@PathVariable("meetingId") Long meetingId) { + Long userId = securityService.getCurrentUserId(); + meetingService.deleteMeeting(meetingId, userId); } @PostMapping("/{meetingId}/participants") diff --git a/src/main/java/net/teumteum/meeting/domain/Meeting.java b/src/main/java/net/teumteum/meeting/domain/Meeting.java index 7efcb9ea..847c6725 100644 --- a/src/main/java/net/teumteum/meeting/domain/Meeting.java +++ b/src/main/java/net/teumteum/meeting/domain/Meeting.java @@ -72,6 +72,10 @@ public boolean isOpen() { return promiseDateTime.isAfter(LocalDateTime.now()); } + public boolean isHost(Long userId) { + return hostUserId.equals(userId); + } + @PrePersist private void assertField() { assertTitle(); @@ -81,21 +85,22 @@ private void assertField() { private void assertIntroduction() { Assert.isTrue(introduction.length() >= 10 && introduction.length() <= 200, - "모임 소개는 10자 ~ 200자 사이가 되어야 합니다. [현재 입력된 모임 소개] : " + introduction); + "모임 소개는 10자 ~ 200자 사이가 되어야 합니다. [현재 입력된 모임 소개] : " + introduction); } private void assertNumberOfRecruits() { - Assert.isTrue(numberOfRecruits >= 3 && numberOfRecruits <= 6, "참여자 수는 3명 ~ 6명 사이가 되어야 합니다. [현재 입력된 참여자 수] : " + numberOfRecruits); + Assert.isTrue(numberOfRecruits >= 3 && numberOfRecruits <= 6, + "참여자 수는 3명 ~ 6명 사이가 되어야 합니다. [현재 입력된 참여자 수] : " + numberOfRecruits); } private void assertTitle() { Assert.isTrue(title.length() >= 2 && title.length() <= 32, - "모임 제목은 2자 ~ 32자 사이가 되어야 합니다. [현재 입력된 모임 제목] : " + title); + "모임 제목은 2자 ~ 32자 사이가 되어야 합니다. [현재 입력된 모임 제목] : " + title); } private void assertParticipantUserIds() { Assert.isTrue(participantUserIds.size() + 1 <= numberOfRecruits, - "최대 참여자 수에 도달한 모임에 참여할 수 없습니다." + "[최대 참여자 수] : " + numberOfRecruits + "[현재 참여자 수] : " - + participantUserIds.size()); + "최대 참여자 수에 도달한 모임에 참여할 수 없습니다." + "[최대 참여자 수] : " + numberOfRecruits + "[현재 참여자 수] : " + + participantUserIds.size()); } } diff --git a/src/main/java/net/teumteum/meeting/service/MeetingService.java b/src/main/java/net/teumteum/meeting/service/MeetingService.java index 11866147..554ee0f3 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingService.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingService.java @@ -1,7 +1,14 @@ package net.teumteum.meeting.service; +import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; -import net.teumteum.meeting.domain.*; +import net.teumteum.meeting.domain.ImageUpload; +import net.teumteum.meeting.domain.Meeting; +import net.teumteum.meeting.domain.MeetingArea; +import net.teumteum.meeting.domain.MeetingRepository; +import net.teumteum.meeting.domain.MeetingSpecification; +import net.teumteum.meeting.domain.Topic; import net.teumteum.meeting.domain.request.CreateMeetingRequest; import net.teumteum.meeting.domain.response.MeetingResponse; import net.teumteum.meeting.domain.response.MeetingsResponse; @@ -13,9 +20,6 @@ import org.springframework.util.Assert; import org.springframework.web.multipart.MultipartFile; -import java.util.List; -import java.util.Set; - @Service @RequiredArgsConstructor public class MeetingService { @@ -28,19 +32,19 @@ public MeetingResponse createMeeting(List images, CreateMeetingRe Assert.isTrue(!images.isEmpty() && images.size() <= 5, "이미지는 1개 이상 5개 이하로 업로드해야 합니다."); Meeting meeting = meetingRepository.save( - Meeting.builder() - .hostUserId(userId) - .title(meetingRequest.title()) - .topic(meetingRequest.topic()) - .introduction(meetingRequest.introduction()) - .meetingArea(MeetingArea.of( - meetingRequest.meetingArea().address(), - meetingRequest.meetingArea().addressDetail()) - ) - .numberOfRecruits(meetingRequest.numberOfRecruits()) - .promiseDateTime(meetingRequest.promiseDateTime()) - .participantUserIds(Set.of(userId)) - .build() + Meeting.builder() + .hostUserId(userId) + .title(meetingRequest.title()) + .topic(meetingRequest.topic()) + .introduction(meetingRequest.introduction()) + .meetingArea(MeetingArea.of( + meetingRequest.meetingArea().address(), + meetingRequest.meetingArea().addressDetail()) + ) + .numberOfRecruits(meetingRequest.numberOfRecruits()) + .promiseDateTime(meetingRequest.promiseDateTime()) + .participantUserIds(Set.of(userId)) + .build() ); uploadMeetingImages(images, meeting); @@ -48,14 +52,6 @@ public MeetingResponse createMeeting(List images, CreateMeetingRe return MeetingResponse.of(meeting); } - private void uploadMeetingImages(List images, Meeting meeting) { - images.forEach( - image -> meeting.getImageUrls().add( - imageUpload.upload(image, meeting.getId().toString()).filePath() - ) - ); - } - @Transactional(readOnly = true) public MeetingResponse getMeetingById(Long meetingId) { var existMeeting = getMeeting(meetingId); @@ -63,9 +59,24 @@ public MeetingResponse getMeetingById(Long meetingId) { return MeetingResponse.of(existMeeting); } + @Transactional + public void deleteMeeting(Long meetingId, Long userId) { + var existMeeting = getMeeting(meetingId); + + if (!existMeeting.isHost(userId)) { + throw new IllegalArgumentException("모임을 삭제할 권한이 없습니다."); + } + if (!existMeeting.isOpen()) { + throw new IllegalArgumentException("종료된 모임은 삭제할 수 없습니다."); + } + + meetingRepository.delete(existMeeting); + } + @Transactional(readOnly = true) - public PageDto getMeetingsBySpecification(Pageable pageable, Topic topic, String meetingAreaStreet, - Long participantUserId, String searchWord, boolean isOpen) { + public PageDto getMeetingsBySpecification(Pageable pageable, Topic topic, + String meetingAreaStreet, + Long participantUserId, String searchWord, boolean isOpen) { Specification spec = MeetingSpecification.withIsOpen(isOpen); @@ -76,8 +87,9 @@ public PageDto getMeetingsBySpecification(Pageable pageable, T } else if (participantUserId != null) { spec = spec.and(MeetingSpecification.withParticipantUserId(participantUserId)); } else if (searchWord != null) { - spec = MeetingSpecification.withSearchWordInTitle(searchWord).or(MeetingSpecification.withSearchWordInIntroduction(searchWord)) - .and(MeetingSpecification.withIsOpen(isOpen)); + spec = MeetingSpecification.withSearchWordInTitle(searchWord) + .or(MeetingSpecification.withSearchWordInIntroduction(searchWord)) + .and(MeetingSpecification.withIsOpen(isOpen)); } var meetings = meetingRepository.findAll(spec, pageable); @@ -116,8 +128,16 @@ public void cancelParticipant(Long meetingId, Long userId) { existMeeting.cancelParticipant(userId); } + private void uploadMeetingImages(List images, Meeting meeting) { + images.forEach( + image -> meeting.getImageUrls().add( + imageUpload.upload(image, meeting.getId().toString()).filePath() + ) + ); + } + private Meeting getMeeting(Long meetingId) { return meetingRepository.findById(meetingId) - .orElseThrow(() -> new IllegalArgumentException("meetingId에 해당하는 모임을 찾을 수 없습니다. \"" + meetingId + "\"")); + .orElseThrow(() -> new IllegalArgumentException("meetingId에 해당하는 모임을 찾을 수 없습니다. \"" + meetingId + "\"")); } } diff --git a/src/main/java/net/teumteum/teum_teum/controller/TeumTeumController.java b/src/main/java/net/teumteum/teum_teum/controller/TeumTeumController.java new file mode 100644 index 00000000..203960bb --- /dev/null +++ b/src/main/java/net/teumteum/teum_teum/controller/TeumTeumController.java @@ -0,0 +1,47 @@ +package net.teumteum.teum_teum.controller; + +import io.sentry.Sentry; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import net.teumteum.core.error.ErrorResponse; +import net.teumteum.teum_teum.domain.request.UserLocationRequest; +import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse; +import net.teumteum.teum_teum.service.TeumTeumService; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/teum-teum") +public class TeumTeumController { + + private final TeumTeumService teumTeumService; + + @PostMapping("/arounds") + @ResponseStatus(HttpStatus.OK) + public UserAroundLocationsResponse getUserAroundLocations( + @Valid @RequestBody UserLocationRequest request) { + return teumTeumService.processingUserAroundLocations(request); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleMethodArgumentNotValidException( + MethodArgumentNotValidException methodArgumentNotValidException) { + Sentry.captureException(methodArgumentNotValidException); + + BindingResult bindingResult = methodArgumentNotValidException.getBindingResult(); + List errors = bindingResult.getAllErrors(); + + return ErrorResponse.of(errors.get(0).getDefaultMessage()); + } +} diff --git a/src/main/java/net/teumteum/teum_teum/domain/UserData.java b/src/main/java/net/teumteum/teum_teum/domain/UserData.java new file mode 100644 index 00000000..e6943563 --- /dev/null +++ b/src/main/java/net/teumteum/teum_teum/domain/UserData.java @@ -0,0 +1,11 @@ +package net.teumteum.teum_teum.domain; + + +public record UserData( + Long id, + String name, + String jobDetailClass, + Long characterId +) { + +} diff --git a/src/main/java/net/teumteum/teum_teum/domain/request/UserLocationRequest.java b/src/main/java/net/teumteum/teum_teum/domain/request/UserLocationRequest.java new file mode 100644 index 00000000..12619f82 --- /dev/null +++ b/src/main/java/net/teumteum/teum_teum/domain/request/UserLocationRequest.java @@ -0,0 +1,25 @@ +package net.teumteum.teum_teum.domain.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import net.teumteum.teum_teum.domain.UserData; + +public record UserLocationRequest( + @NotNull(message = "경도는 필수 입력값입니다.") + Double longitude, + @NotNull(message = "위도는 필수 입력값입니다.") + Double latitude, + @NotNull(message = "id 는 필수 입력값입니다.") + Long id, + @NotBlank(message = "이름은 필수 입력값입니다.") + String name, + @NotBlank(message = "직무는 필수 입력값입니다.") + String jobDetailClass, + @NotNull(message = "캐릭터 id 는 필수 입력값입니다.") + Long characterId +) { + + public UserData toUserData() { + return new UserData(id, name, jobDetailClass, characterId); + } +} diff --git a/src/main/java/net/teumteum/teum_teum/domain/response/UserAroundLocationsResponse.java b/src/main/java/net/teumteum/teum_teum/domain/response/UserAroundLocationsResponse.java new file mode 100644 index 00000000..db9363d2 --- /dev/null +++ b/src/main/java/net/teumteum/teum_teum/domain/response/UserAroundLocationsResponse.java @@ -0,0 +1,36 @@ +package net.teumteum.teum_teum.domain.response; + +import java.util.List; +import net.teumteum.teum_teum.domain.UserData; + +public record UserAroundLocationsResponse( + List userLocations +) { + + public static UserAroundLocationsResponse of(List userData) { + return new UserAroundLocationsResponse( + userData.stream() + .map(UserAroundLocationResponse::of) + .toList() + ); + } + + public record UserAroundLocationResponse( + Long id, + String name, + String jobDetailClass, + Long characterId + ) { + + public static UserAroundLocationResponse of( + UserData userData + ) { + return new UserAroundLocationResponse( + userData.id(), + userData.name(), + userData.jobDetailClass(), + userData.characterId() + ); + } + } +} diff --git a/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java b/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java new file mode 100644 index 00000000..9dd1e21e --- /dev/null +++ b/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java @@ -0,0 +1,101 @@ +package net.teumteum.teum_teum.service; + +import static java.lang.System.currentTimeMillis; +import static java.time.Duration.ofMinutes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.teumteum.teum_teum.domain.UserData; +import net.teumteum.teum_teum.domain.request.UserLocationRequest; +import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse; +import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse.UserAroundLocationResponse; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.core.GeoOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.domain.geo.Metrics; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TeumTeumService { + + private static final String KEY = "userLocation"; + private static final int SEARCH_LIMIT = 6; + private static final Duration LOCATION_EXPIRATION = ofMinutes(1); + + private final ObjectMapper objectMapper; + + private final RedisTemplate redisTemplate; + + public UserAroundLocationsResponse processingUserAroundLocations(UserLocationRequest request) { + GeoOperations geoValueOperations = redisTemplate.opsForGeo(); + + String userDataJson = null; + try { + userDataJson = objectMapper.writeValueAsString( + request.toUserData()) + ":" + currentTimeMillis(); + } catch (JsonProcessingException e) { + log.error("JsonProcessingException Occurred!"); + } + + geoValueOperations.add(KEY, new Point(request.longitude(), request.latitude()), userDataJson); + + return getUserAroundLocations(geoValueOperations, request.longitude(), request.latitude()); + } + + private UserAroundLocationsResponse getUserAroundLocations(GeoOperations geoValueOperations, + Double longitude, Double latitude) { + + GeoResults> geoResults + = geoValueOperations.radius(KEY, + new Circle(new Point(longitude, latitude), new Distance(100, Metrics.METERS))); + + return getUserAroundLocationsResponse(geoResults); + } + + private UserAroundLocationsResponse getUserAroundLocationsResponse(GeoResults> geoResults) { + + List userAroundLocationResponses = new ArrayList<>(); + + long currentTime = currentTimeMillis(); + int count = 0; + + for (GeoResult> geoResult : Objects.requireNonNull(geoResults)) { + String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[1]; + long timestamp = Long.parseLong(userSavedTime); + + if (currentTime - timestamp < LOCATION_EXPIRATION.toMillis()) { + String userDataJson = String.valueOf(geoResult.getContent().getName()).split(":")[0]; + UserData userData = null; + try { + userData = objectMapper.readValue(userDataJson, UserData.class); + } catch (JsonProcessingException e) { + log.error("JsonProcessingException Occurred!"); + } + + UserAroundLocationResponse userAroundLocationResponse + = UserAroundLocationResponse.of(Objects.requireNonNull(userData)); + + userAroundLocationResponses.add(userAroundLocationResponse); + count++; + + if (count >= SEARCH_LIMIT) { + break; + } + } + } + return new UserAroundLocationsResponse(userAroundLocationResponses); + } +} diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index f7255e69..3b14dfd1 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -1,27 +1,42 @@ package net.teumteum.user.controller; import io.sentry.Sentry; +import jakarta.validation.Valid; +import java.util.Arrays; +import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.error.ErrorResponse; import net.teumteum.core.security.service.SecurityService; -import net.teumteum.user.domain.response.UserMeGetResponse; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; -import net.teumteum.user.domain.response.*; +import net.teumteum.user.domain.response.FriendsResponse; +import net.teumteum.user.domain.response.InterestQuestionResponse; +import net.teumteum.user.domain.response.UserGetResponse; +import net.teumteum.user.domain.response.UserMeGetResponse; +import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UsersGetByIdResponse; import net.teumteum.user.service.UserService; -import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import java.util.Arrays; -import java.util.List; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/users") public class UserController { - private final ApplicationContext applicationContext; private final UserService userService; private final SecurityService securityService; @@ -49,7 +64,7 @@ public UserMeGetResponse getMe() { @PutMapping @ResponseStatus(HttpStatus.OK) - public void updateUser(@RequestBody UserUpdateRequest request) { + public void updateUser(@Valid @RequestBody UserUpdateRequest request) { userService.updateUser(getCurrentUserId(), request); } @@ -80,7 +95,7 @@ public void withdraw() { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserRegisterResponse register(@RequestBody UserRegisterRequest request) { + public UserRegisterResponse register(@Valid @RequestBody UserRegisterRequest request) { return userService.register(request); } @@ -97,6 +112,18 @@ public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ill return ErrorResponse.of(illegalArgumentException); } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleMethodArgumentNotValidException( + MethodArgumentNotValidException methodArgumentNotValidException) { + Sentry.captureException(methodArgumentNotValidException); + + BindingResult bindingResult = methodArgumentNotValidException.getBindingResult(); + List errors = bindingResult.getAllErrors(); + + return ErrorResponse.of(errors.get(0).getDefaultMessage()); + } + private Long getCurrentUserId() { return securityService.getCurrentUserId(); } diff --git a/src/main/java/net/teumteum/user/domain/User.java b/src/main/java/net/teumteum/user/domain/User.java index a022e769..5943d2fb 100644 --- a/src/main/java/net/teumteum/user/domain/User.java +++ b/src/main/java/net/teumteum/user/domain/User.java @@ -54,8 +54,8 @@ public class User extends TimeBaseEntity { @Column(name = "role_type") private RoleType roleType; - @Embedded - private ActivityArea activityArea; + @Column(name = "activity_area") + private String activityArea; @Column(name = "mbti", length = 4) private String mbti; diff --git a/src/main/java/net/teumteum/user/domain/request/UserRegisterRequest.java b/src/main/java/net/teumteum/user/domain/request/UserRegisterRequest.java index 4ef7e66b..d6120b0b 100644 --- a/src/main/java/net/teumteum/user/domain/request/UserRegisterRequest.java +++ b/src/main/java/net/teumteum/user/domain/request/UserRegisterRequest.java @@ -2,25 +2,47 @@ import static net.teumteum.user.domain.RoleType.ROLE_USER; -import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.util.List; import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.JobStatus; import net.teumteum.user.domain.OAuth; import net.teumteum.user.domain.User; +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public record UserRegisterRequest( + @NotBlank(message = "id 값은 필수 입력값입니다.") String id, + @NotNull(message = "동의 항목은 필수 입력값입니다.") Terms terms, + @NotBlank(message = "이름은 필수 입력값입니다.") String name, + @NotBlank(message = "생년월일은 필수 입력값입니다.") String birth, + @NotNull(message = "캐릭터 아이디는 필수 입력값입니다.") Long characterId, + @NotNull(message = "소셜 로그인 타입은 필수 입력값입니다.") Authenticated authenticated, - ActivityArea activityArea, + @NotBlank(message = "관심 지역은 필수 입력값입니다.") + String activityArea, + @NotBlank(message = "mbti 는 필수 입력값입니다.") String mbti, + @NotNull(message = "현재 상태는 필수 입력값입니다.") String status, + @Valid + @NotNull(message = "직업 관련 값은 필수 입력값입니다.") Job job, + @NotEmpty(message = "관심 항목은 최소 1개을 입력해야합니다.") + @Size(max = 3, message = "관심 항목은 최대 3개까지 입력가능합니다.") List interests, + @Size(max = 50) + @NotBlank(message = "목표는 필수 입력값입니다.") String goal ) { @@ -36,10 +58,7 @@ public User toUser() { authenticated ), ROLE_USER, - new net.teumteum.user.domain.ActivityArea( - activityArea.city, - activityArea.street - ), + activityArea, mbti, JobStatus.valueOf(status), goal, @@ -58,6 +77,7 @@ public User toUser() { ); } + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public record Terms( boolean service, boolean privatePolicy @@ -65,17 +85,13 @@ public record Terms( } - public record ActivityArea( - String city, - List street - ) { - - } - + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public record Job( - @JsonInclude(JsonInclude.Include.NON_NULL) String name, + @JsonProperty("class") + @NotBlank(message = "직군은 필수 입력값입니다.") String jobClass, + @NotBlank(message = "직무는 필수 입력값입니다.") String detailClass ) { diff --git a/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java b/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java index 05ba8ec8..c3bb7627 100644 --- a/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java +++ b/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java @@ -3,9 +3,13 @@ import static net.teumteum.user.domain.RoleType.ROLE_USER; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.util.List; import java.util.Set; -import net.teumteum.user.domain.ActivityArea; import net.teumteum.user.domain.Job; import net.teumteum.user.domain.JobStatus; import net.teumteum.user.domain.OAuth; @@ -13,15 +17,28 @@ import net.teumteum.user.domain.User; public record UserUpdateRequest( + @NotNull(message = "id 값은 필수 입력값입니다.") Long id, + @NotBlank(message = "이름은 필수 입력값입니다.") String newName, + @NotBlank(message = "생년월일은 필수 입력값입니다.") String newBirth, + @NotNull(message = "캐릭터 아이디는 필수 입력값입니다.") Long newCharacterId, - NewActivityArea newActivityArea, + @NotBlank(message = "관심 지역은 필수 입력값입니다.") + String newActivityArea, + @NotBlank(message = "mbti 는 필수 입력값입니다.") String newMbti, + @NotNull(message = "현재 상태는 필수 입력값입니다.") String newStatus, + @Size(max = 50) + @NotBlank(message = "목표는 필수 입력값입니다.") String newGoal, + @Valid + @NotNull(message = "직업 관련 값은 필수 입력값입니다.") NewJob newJob, + @Size(max = 3, message = "관심 항목은 최대 3개까지 입력가능합니다.") + @NotEmpty(message = "관심 항목은 최소 1개을 입력해야합니다.") List newInterests ) { @@ -41,10 +58,7 @@ public User toUser() { IGNORE_MANNER_TEMPERATURE, IGNORE_O_AUTH, ROLE_USER, - new ActivityArea( - newActivityArea.city, - newActivityArea.streets - ), + newActivityArea, newMbti, JobStatus.valueOf(newStatus), newGoal, @@ -60,17 +74,12 @@ public User toUser() { ); } - public record NewActivityArea( - String city, - List streets - ) { - - } - public record NewJob( String name, @JsonProperty("class") + @NotBlank(message = "직군은 필수 입력값입니다.") String jobClass, + @NotBlank(message = "직무는 필수 입력값입니다.") String detailClass ) { diff --git a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java index ccf69252..34ee5b5d 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; - import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; @@ -13,7 +12,7 @@ public record UserGetResponse( Long characterId, int mannerTemperature, Authenticated authenticated, - ActivityArea activityArea, + String activityArea, String mbti, String status, String goal, @@ -29,7 +28,7 @@ public static UserGetResponse of(User user) { user.getCharacterId(), user.getMannerTemperature(), user.getOauth().getAuthenticated(), - ActivityArea.of(user), + user.getActivityArea(), user.getMbti(), user.getStatus().name(), user.getGoal(), @@ -38,19 +37,6 @@ public static UserGetResponse of(User user) { ); } - public record ActivityArea( - String city, - List streets - ) { - - public static ActivityArea of(User user) { - return new ActivityArea( - user.getActivityArea().getCity(), - user.getActivityArea().getStreet() - ); - } - - } public record Job( String name, diff --git a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java index a9f9b9eb..99d827ef 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java @@ -1,73 +1,38 @@ package net.teumteum.user.domain.response; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; -import java.util.List; - public record UserMeGetResponse( - Long id, - String name, - String birth, - Long characterId, - int mannerTemperature, - Authenticated authenticated, - ActivityArea activityArea, - String mbti, - String status, - String goal, - Job job, - List interests + Long id, + String name, + String birth, + Long characterId, + int mannerTemperature, + Authenticated authenticated, + String activityArea, + String mbti, + String status, + String goal, + Job job, + List interests ) { public static UserMeGetResponse of(User user) { - return new UserMeGetResponse( - user.getId(), - user.getName(), - user.getBirth(), - user.getCharacterId(), - user.getMannerTemperature(), - user.getOauth().getAuthenticated(), - ActivityArea.of(user), - user.getMbti(), - user.getStatus().name(), - user.getGoal(), - Job.of(user), - user.getInterests() - ); + return new UserMeGetResponse(user.getId(), user.getName(), user.getBirth(), user.getCharacterId(), + user.getMannerTemperature(), user.getOauth().getAuthenticated(), user.getActivityArea(), user.getMbti(), + user.getStatus().name(), user.getGoal(), Job.of(user), user.getInterests()); } - public record ActivityArea( - String city, - List streets - ) { - - public static ActivityArea of(User user) { - return new ActivityArea( - user.getActivityArea().getCity(), - user.getActivityArea().getStreet() - ); - } - - } - public record Job( - String name, - boolean certificated, - @JsonProperty("class") - String jobClass, - String detailClass - ) { + public record Job(String name, boolean certificated, @JsonProperty("class") String jobClass, String detailClass) { public static Job of(User user) { - return new Job( - user.getJob().getName(), - user.getJob().isCertificated(), - user.getJob().getJobClass(), - user.getJob().getDetailJobClass() - ); + return new Job(user.getJob().getName(), user.getJob().isCertificated(), user.getJob().getJobClass(), + user.getJob().getDetailJobClass()); } } } diff --git a/src/main/java/net/teumteum/user/domain/response/UserRegisterResponse.java b/src/main/java/net/teumteum/user/domain/response/UserRegisterResponse.java index 7fc204b0..5eb7cfca 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserRegisterResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserRegisterResponse.java @@ -1,7 +1,14 @@ package net.teumteum.user.domain.response; +import net.teumteum.auth.domain.response.TokenResponse; + public record UserRegisterResponse( - Long id + Long id, + String accessToken, + String refreshToken ) { + public static UserRegisterResponse of(Long id, TokenResponse tokenResponse) { + return new UserRegisterResponse(id, tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + } } diff --git a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java index a21841e7..40406e2d 100644 --- a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; - import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; @@ -24,7 +23,7 @@ public record UserGetResponse( Long characterId, int mannerTemperature, Authenticated authenticated, - ActivityArea activityArea, + String activityArea, String mbti, String status, String goal, @@ -40,7 +39,7 @@ public static UserGetResponse of(User user) { user.getCharacterId(), user.getMannerTemperature(), user.getOauth().getAuthenticated(), - ActivityArea.of(user), + user.getActivityArea(), user.getMbti(), user.getStatus().name(), user.getGoal(), @@ -49,20 +48,6 @@ public static UserGetResponse of(User user) { ); } - public record ActivityArea( - String city, - List streets - ) { - - public static ActivityArea of(User user) { - return new ActivityArea( - user.getActivityArea().getCity(), - user.getActivityArea().getStreet() - ); - } - - } - public record Job( String name, boolean certificated, diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 90849cf2..808318bb 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -3,6 +3,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.security.Authenticated; +import net.teumteum.core.security.service.JwtService; import net.teumteum.core.security.service.RedisService; import net.teumteum.core.security.service.SecurityService; import net.teumteum.user.domain.BalanceGameType; @@ -29,6 +30,7 @@ public class UserService { private final UserRepository userRepository; private final InterestQuestion interestQuestion; private final RedisService redisService; + private final JwtService jwtService; public UserGetResponse getUserById(Long userId) { var existUser = getUser(userId); @@ -79,13 +81,13 @@ public void withdraw(Long userId) { @Transactional public UserRegisterResponse register(UserRegisterRequest request) { checkUserExistence(request.authenticated(), request.id()); + User savedUser = userRepository.save(request.toUser()); - return new UserRegisterResponse(userRepository.save(request.toUser()).getId()); + return UserRegisterResponse.of(savedUser.getId(), jwtService.createServiceToken(savedUser)); } @Transactional public void logout(Long userId) { - getUser(userId); redisService.deleteData(String.valueOf(userId)); SecurityService.clearSecurityContext(); } diff --git a/src/main/resources/application-auth.yml b/src/main/resources/application-auth.yml index da4b5d77..204305b8 100644 --- a/src/main/resources/application-auth.yml +++ b/src/main/resources/application-auth.yml @@ -18,7 +18,7 @@ spring: naver: client-id: ${NAVER_CLIENT_ID} - client-secret: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_CLIENT_SECRET} redirect-uri: ${NAVER_REDIRECT_URI} authorization-grant-type: authorization_code scope: diff --git a/src/main/resources/application-datasource.yml b/src/main/resources/application-datasource.yml index c3305ef8..a0e139df 100644 --- a/src/main/resources/application-datasource.yml +++ b/src/main/resources/application-datasource.yml @@ -6,7 +6,7 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: + url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} hikari: @@ -14,7 +14,7 @@ spring: maximum-pool-size: 80 flyway: - url: + url: ${DATABASE_URL} user: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} baseline-on-migrate: true diff --git a/src/main/resources/db/migration/V5__update_users.sql b/src/main/resources/db/migration/V5__update_users.sql new file mode 100644 index 00000000..6d4a354c --- /dev/null +++ b/src/main/resources/db/migration/V5__update_users.sql @@ -0,0 +1,8 @@ +drop table users_interests; + +alter table users + drop column city; + +alter table users + add column activity_area varchar(255); + diff --git a/src/main/resources/db/migration/V6__create_users_interests.sql b/src/main/resources/db/migration/V6__create_users_interests.sql new file mode 100644 index 00000000..f7cd3356 --- /dev/null +++ b/src/main/resources/db/migration/V6__create_users_interests.sql @@ -0,0 +1,8 @@ +create table if not exists users_interests +( + users_id bigint not null, + interests varchar(255), + foreign key (users_id) references users (id) +); + +drop table users_street; diff --git a/src/main/resources/favicon.ico b/src/main/resources/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/net/teumteum/core/property/PropertyTest.java b/src/test/java/net/teumteum/core/property/PropertyTest.java index b86415ba..a367cdd1 100644 --- a/src/test/java/net/teumteum/core/property/PropertyTest.java +++ b/src/test/java/net/teumteum/core/property/PropertyTest.java @@ -38,7 +38,7 @@ class Read_redis_value_from_application_yml { void Make_redis_property_from_application_yml() { // given String expectedHost = "localhost"; - int expectedPort = 6378; + int expectedPort = 6379; // when & then Assertions.assertEquals(expectedHost, redisProperty.getHost()); @@ -55,11 +55,10 @@ class Read_jwt_value_from_application_yml { void Make_jwt_property_from_application_yml() { // given String expectedBearer = "Bearer"; - String expectedSecret = "secret"; // when & then Assertions.assertEquals(expectedBearer, jwtProperty.getBearer()); - Assertions.assertEquals(expectedSecret, jwtProperty.getSecret()); + Assertions.assertNotNull(jwtProperty.getSecret()); } } } diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 693b956d..1d6f66e8 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -3,6 +3,7 @@ import java.util.List; import net.teumteum.meeting.config.PageableHandlerMethodArgumentResolver; import net.teumteum.meeting.domain.Topic; +import net.teumteum.teum_teum.domain.request.UserLocationRequest; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; import org.springframework.boot.test.context.TestComponent; @@ -151,4 +152,21 @@ ResponseSpec logoutUser(String accessToken) { .header(HttpHeaders.AUTHORIZATION, accessToken) .exchange(); } + + ResponseSpec getTeumteumAround(String accessToken, UserLocationRequest request) { + return webTestClient + .post() + .uri("/teum-teum/arounds") + .header(HttpHeaders.AUTHORIZATION, accessToken) + .bodyValue(request) + .exchange(); + } + + ResponseSpec deleteMeeting(String accessToken, Long meetingId) { + return webTestClient + .delete() + .uri("/meetings/" + meetingId) + .header(HttpHeaders.AUTHORIZATION, accessToken) + .exchange(); + } } diff --git a/src/test/java/net/teumteum/integration/IntegrationTest.java b/src/test/java/net/teumteum/integration/IntegrationTest.java index 70237b6e..070122f5 100644 --- a/src/test/java/net/teumteum/integration/IntegrationTest.java +++ b/src/test/java/net/teumteum/integration/IntegrationTest.java @@ -18,6 +18,7 @@ @ContextConfiguration(classes = { Api.class, Repository.class, + RedisRepository.class, Application.class, GptTestServer.class, TestLoginContext.class @@ -30,10 +31,11 @@ abstract public class IntegrationTest { @Autowired protected Repository repository; - @Autowired protected TestLoginContext loginContext; + @Autowired + protected RedisRepository redisRepository; @AfterEach @BeforeEach diff --git a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java index 20a6348a..b02e288e 100644 --- a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java @@ -42,11 +42,11 @@ void Return_meeting_info_if_exist_meeting_id_received() { var result = api.getMeetingById(VALID_TOKEN, meeting.getId()); // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(MeetingResponse.class) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(MeetingResponse.class) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -58,7 +58,56 @@ void Return_400_bad_request_if_not_exists_meeting_id_received() { var result = api.getMeetingById(VALID_TOKEN, notExistMeetingId); // then result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class); + .expectBody(ErrorResponse.class); + } + } + + @Nested + @DisplayName("미팅 삭제 API는") + class Delete_meeting_api { + + @Test + @DisplayName("존재하는 모임의 id가 주어지면, 모임을 삭제한다.") + void Delete_meeting_if_exist_meeting_id_received() { + // given + var host = repository.saveAndGetUser(); + loginContext.setUserId(host.getId()); + + var meeting = repository.saveAndGetOpenMeetingWithHostId(host.getId()); + // when + var result = api.deleteMeeting(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isOk(); + } + + @Test + @DisplayName("종료된 모임의 id가 주어지면, 400 Bad Request를 응답한다.") + void Return_400_bad_request_if_closed_meeting_id_received() { + // given + var host = repository.saveAndGetUser(); + loginContext.setUserId(host.getId()); + var meeting = repository.saveAndGetCloseMeetingWithHostId(host.getId()); + // when + var result = api.deleteMeeting(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class); + } + + @Test + @DisplayName("hostId와 userId가 다르면, 400 Bad Request를 응답한다.") + void Return_400_bad_request_if_hostId_and_userId_are_different() { + // given + var user = repository.saveAndGetUser(); + loginContext.setUserId(user.getId()); + + var host = repository.saveAndGetUser(); + var meeting = repository.saveAndGetOpenMeetingWithHostId(host.getId()); + // when + var result = api.deleteMeeting(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class); } } @@ -75,9 +124,9 @@ void Return_meeting_list_if_topic_and_page_nation_received() { var closeTopicMeetingsByTopic = repository.saveAndGetOpenMeetingsByTopic(size, Topic.고민_나누기); var expectedData = MeetingsResponse.of( - openMeetingsByTopic.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() + openMeetingsByTopic.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() ); var expected = PageDto.of(expectedData, false); @@ -86,12 +135,12 @@ void Return_meeting_list_if_topic_and_page_nation_received() { var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -102,14 +151,14 @@ void Return_meeting_list_if_search_word_and_page_nation_received() { var openMeetingsByTitle = repository.saveAndGetOpenMeetingsByTitle(size, "개발자 스터디"); var closeMeetingsByTitle = repository.saveAndGetCloseMeetingsByTitle(size, "개발자 스터디"); var openMeetingsByIntroduction = repository.saveAndGetOpenMeetingsByIntroduction(size, - "개발자 스터디에 대한 설명입니다."); + "개발자 스터디에 대한 설명입니다."); var closeMeetingsByIntroduction = repository.saveAndGetCloseMeetingsByIntroduction(size, - "개발자 스터디에 대한 설명입니다."); + "개발자 스터디에 대한 설명입니다."); var expectedData = MeetingsResponse.of(Stream.of(openMeetingsByIntroduction, openMeetingsByTitle) - .flatMap(Collection::stream) - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() + .flatMap(Collection::stream) + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() ); var expected = PageDto.of(expectedData, false); @@ -118,12 +167,12 @@ void Return_meeting_list_if_search_word_and_page_nation_received() { var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -135,9 +184,9 @@ void Return_meeting_list_if_participant_user_id_and_page_nation_received() { var closeMeetingsByParticipantUserId = repository.saveAndGetCloseMeetingsByParticipantUserId(size, 2L); var expectedData = MeetingsResponse.of( - openMeetingsByParticipantUserId.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() + openMeetingsByParticipantUserId.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() ); var expected = PageDto.of(expectedData, false); @@ -146,12 +195,12 @@ void Return_meeting_list_if_participant_user_id_and_page_nation_received() { var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -162,10 +211,10 @@ void Return_has_next_true_if_more_data_exists_than_requested_size_and_page() { var openMeetingsByTopic = repository.saveAndGetOpenMeetingsByTopic(size, Topic.스터디); var expectedData = MeetingsResponse.of( - openMeetingsByTopic.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() - .subList(0, DEFAULT_QUERY_SIZE) + openMeetingsByTopic.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() + .subList(0, DEFAULT_QUERY_SIZE) ); var expected = PageDto.of(expectedData, true); @@ -174,12 +223,12 @@ void Return_has_next_true_if_more_data_exists_than_requested_size_and_page() { var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } } @@ -199,13 +248,13 @@ void Join_meeting_if_exist_meeting_id_received() { var result = api.joinMeeting(VALID_TOKEN, existMeeting.getId()); // then Assertions.assertThat( - result.expectStatus().isCreated() - .expectBody(MeetingResponse.class) - .returnResult() - .getResponseBody()) - .extracting(MeetingResponse::participantIds) - .has(new Condition<>(ids -> ids.contains(me.getId()), "참여자 목록에 나를 포함한다.") - ); + result.expectStatus().isCreated() + .expectBody(MeetingResponse.class) + .returnResult() + .getResponseBody()) + .extracting(MeetingResponse::participantIds) + .has(new Condition<>(ids -> ids.contains(me.getId()), "참여자 목록에 나를 포함한다.") + ); } @Test @@ -221,7 +270,7 @@ void Return_400_bad_request_if_already_joined_meeting_id_received() { var result = api.joinMeeting(VALID_TOKEN, meeting.getId()); // then result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class); + .expectBody(ErrorResponse.class); } @Test @@ -236,7 +285,7 @@ void Return_400_bad_request_if_closed_meeting_id_received() { var result = api.joinMeeting(VALID_TOKEN, meeting.getId()); // then result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class); + .expectBody(ErrorResponse.class); } @Test @@ -249,7 +298,7 @@ void Return_400_bad_request_if_exceed_max_number_of_recruits_meeting_id_received var result = api.joinMeeting(VALID_TOKEN, meeting.getId()); // then result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class); + .expectBody(ErrorResponse.class); } } @@ -284,12 +333,12 @@ void Return_400_bad_request_if_not_joined_meeting_id_received() { var result = api.cancelMeeting(VALID_TOKEN, meeting.getId()); // then Assertions.assertThat(result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class) - .returnResult() - .getResponseBody() - ) - .extracting(ErrorResponse::getMessage) - .isEqualTo("참여하지 않은 모임입니다."); + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody() + ) + .extracting(ErrorResponse::getMessage) + .isEqualTo("참여하지 않은 모임입니다."); } @Test @@ -304,12 +353,12 @@ void Return_400_bad_request_if_closed_meeting_id_received() { var result = api.cancelMeeting(VALID_TOKEN, meeting.getId()); // then Assertions.assertThat(result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class) - .returnResult() - .getResponseBody() - ) - .extracting(ErrorResponse::getMessage) - .isEqualTo("종료된 모임에서 참여를 취소할 수 없습니다."); + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody() + ) + .extracting(ErrorResponse::getMessage) + .isEqualTo("종료된 모임에서 참여를 취소할 수 없습니다."); } } } diff --git a/src/test/java/net/teumteum/integration/RedisRepository.java b/src/test/java/net/teumteum/integration/RedisRepository.java new file mode 100644 index 00000000..2660ed9d --- /dev/null +++ b/src/test/java/net/teumteum/integration/RedisRepository.java @@ -0,0 +1,41 @@ +package net.teumteum.integration; + +import lombok.RequiredArgsConstructor; +import net.teumteum.core.security.service.RedisService; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.core.GeoOperations; +import org.springframework.data.redis.core.RedisTemplate; + +@TestComponent +@RequiredArgsConstructor +public class RedisRepository { + + private final RedisService redisService; + private final RedisTemplate redisTemplate; + + public void saveGeoRedisData(String key, Point point, String member) { + GeoOperations geoOperations = redisTemplate.opsForGeo(); + geoOperations.add(key, point, member); + } + + public GeoResults> getGeoRedisData(String key, Circle circle) { + GeoOperations geoOperations = redisTemplate.opsForGeo(); + return geoOperations.radius(key, circle); + } + + public void saveRedisDataWithExpiration(String key, String value, Long duration) { + redisService.setDataWithExpiration(key, value, duration); + } + + void deleteRedisData(String key) { + redisService.deleteData(key); + } + + public String getRedisData(String key) { + return redisService.getData(key); + } +} diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index ec799625..a945b29c 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -1,7 +1,6 @@ package net.teumteum.integration; -import jakarta.persistence.EntityManager; import java.util.List; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; @@ -16,11 +15,11 @@ import net.teumteum.user.domain.UserRepository; import org.springframework.boot.test.context.TestComponent; import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; @TestComponent @Import(AppConfig.class) @RequiredArgsConstructor - public class Repository { private final UserRepository userRepository; @@ -29,6 +28,8 @@ public class Repository { private final RedisService redisService; + private final RedisTemplate redisTemplate; + public User saveAndGetUser() { var user = UserFixture.getNullIdUser(); return userRepository.saveAndFlush(user); @@ -53,6 +54,16 @@ Meeting saveAndGetCloseMeeting() { return meetingRepository.saveAndFlush(meeting); } + Meeting saveAndGetOpenMeetingWithHostId(Long hostId) { + var meeting = MeetingFixture.getOpenMeetingWithHostId(hostId); + return meetingRepository.saveAndFlush(meeting); + } + + Meeting saveAndGetCloseMeetingWithHostId(Long hostId) { + var meeting = MeetingFixture.getCloseMeetingWithHostId(hostId); + return meetingRepository.saveAndFlush(meeting); + } + Meeting saveAndGetOpenFullMeeting() { var meeting = MeetingFixture.getOpenFullMeeting(); return meetingRepository.saveAndFlush(meeting); @@ -122,18 +133,6 @@ List saveAndGetOpenMeetings(int size) { return meetingRepository.saveAllAndFlush(meetings); } - void saveRedisDataWithExpiration(String key, String value, Long duration) { - redisService.setDataWithExpiration(key, value, duration); - } - - void deleteRedisData(String key) { - redisService.deleteData(key); - } - - void getRedisData(String key) { - redisService.getData(key); - } - void clear() { userRepository.deleteAll(); meetingRepository.deleteAll(); diff --git a/src/test/java/net/teumteum/integration/RequestFixture.java b/src/test/java/net/teumteum/integration/RequestFixture.java index 3f288ccd..28a35222 100644 --- a/src/test/java/net/teumteum/integration/RequestFixture.java +++ b/src/test/java/net/teumteum/integration/RequestFixture.java @@ -2,53 +2,57 @@ import java.util.UUID; import net.teumteum.core.security.Authenticated; -import net.teumteum.user.domain.Job; import net.teumteum.user.domain.User; import net.teumteum.user.domain.request.UserRegisterRequest; -import net.teumteum.user.domain.request.UserRegisterRequest.ActivityArea; +import net.teumteum.user.domain.request.UserRegisterRequest.Job; import net.teumteum.user.domain.request.UserRegisterRequest.Terms; import net.teumteum.user.domain.request.UserUpdateRequest; -import net.teumteum.user.domain.request.UserUpdateRequest.NewActivityArea; import net.teumteum.user.domain.request.UserUpdateRequest.NewJob; public class RequestFixture { public static UserUpdateRequest userUpdateRequest(User user) { return new UserUpdateRequest(user.getId(), "new_name", user.getBirth(), user.getCharacterId(), - newActivityArea(user), user.getMbti(), user.getStatus().name(), user.getGoal(), newJob(user), + user.getActivityArea(), user.getMbti(), user.getStatus().name(), user.getGoal(), newJob(user), user.getInterests()); } - private static NewActivityArea newActivityArea(User user) { - return new NewActivityArea(user.getActivityArea().getCity(), user.getActivityArea().getStreet()); - } - private static NewJob newJob(User user) { return new NewJob(user.getJob().getName(), user.getJob().getJobClass(), user.getJob().getDetailJobClass()); } + public static UserRegisterRequest userRegisterRequest(User user) { return new UserRegisterRequest(UUID.randomUUID().toString(), - new Terms(user.getTerms().getService(), user.getTerms().getPrivacyPolicy()), user.getName(), - user.getBirth(), user.getCharacterId(), Authenticated.카카오, activityArea(user), - user.getMbti(), user.getStatus().name(), new UserRegisterRequest.Job("직장인", "디자인", "BX 디자이너"), + terms(user), user.getName(), + user.getBirth(), user.getCharacterId(), Authenticated.카카오, user.getActivityArea(), + user.getMbti(), user.getStatus().name(), job(user), user.getInterests(), user.getGoal()); } public static UserRegisterRequest userRegisterRequestWithFail(User user) { return new UserRegisterRequest(user.getOauth().getOauthId(), - new Terms(user.getTerms().getService(), user.getTerms().getPrivacyPolicy()), user.getName(), - user.getBirth(), user.getCharacterId(), user.getOauth().getAuthenticated(), activityArea(user), - user.getMbti(), user.getStatus().name(), new UserRegisterRequest.Job("직장인", "디자인", "BX 디자이너"), + terms(user), user.getName(), + user.getBirth(), user.getCharacterId(), user.getOauth().getAuthenticated(), user.getActivityArea(), + user.getMbti(), user.getStatus().name(), job(user), user.getInterests(), user.getGoal()); } - private static ActivityArea activityArea(User user) { - return new ActivityArea(user.getActivityArea().getCity(), user.getActivityArea().getStreet()); + public static UserRegisterRequest userRegisterRequestWithNoValid(User user) { + return new UserRegisterRequest(user.getOauth().getOauthId(), + terms(user), null, + user.getBirth(), user.getCharacterId(), user.getOauth().getAuthenticated(), user.getActivityArea(), + user.getMbti(), user.getStatus().name(), job(user), + null, user.getGoal()); } private static Job job(User user) { - return new Job(user.getJob().getName(), false, user.getJob().getJobClass(), user.getJob().getDetailJobClass()); + return new Job(user.getJob().getName(), + user.getJob().getJobClass(), + user.getJob().getDetailJobClass()); } + private static Terms terms(User user) { + return new Terms(user.getTerms().getService(), user.getTerms().getPrivacyPolicy()); + } } diff --git a/src/test/java/net/teumteum/integration/TeumTeumIntegrationTest.java b/src/test/java/net/teumteum/integration/TeumTeumIntegrationTest.java new file mode 100644 index 00000000..a207aaad --- /dev/null +++ b/src/test/java/net/teumteum/integration/TeumTeumIntegrationTest.java @@ -0,0 +1,61 @@ +package net.teumteum.integration; + +import java.util.List; +import net.teumteum.teum_teum.UserLocationFixture; +import net.teumteum.teum_teum.domain.UserData; +import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.data.geo.Point; +import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +@DisplayName("틈틈 서비스 통합테스트의") +public class TeumTeumIntegrationTest extends IntegrationTest { + + @Nested + @DisplayName("회원 위치 저장 로직은") + class Save_user_location_logic { + + private static final String VALID_TOKEN = "VALID_TOKEN"; + private static final String INVALID_TOKEN = "IN_VALID_TOKEN"; + private static final String KEY = "userLocation"; + private static final long currentTimeMillis = 2000L; + + @BeforeEach + void init() throws JsonProcessingException { + Point point01 = new Point(120.3, -22.4); + Point point02 = new Point(120.4, -22.2); + + UserData userData01 = new UserData(100L, "Selly", "frontend", 1L); + UserData userData02 = new UserData(101L, "Tom", "UX design", 5L); + + redisRepository.saveGeoRedisData(KEY, point01, + new ObjectMapper().writeValueAsString(userData01 + ":" + currentTimeMillis)); + + redisRepository.saveGeoRedisData(KEY, point02, + new ObjectMapper().writeValueAsString(userData02 + ":" + currentTimeMillis)); + } + + @Test + @DisplayName("회원의 올바른 요청이 오는 경우, 정상적으로 저장하고, 100 m 이내의 유저 정보를 가져온다.") + void if_user_request_valid_save_successfully() { + // given + var userLocationRequest = UserLocationFixture.getDefaultUserLocationRequest(); + + var expected = List.of(UserLocationFixture.getDefaultUserAroundLocationResponse()); + // when + var result = api.getTeumteumAround(VALID_TOKEN, userLocationRequest); + + // then + Assertions.assertThat( + result.expectBody(UserAroundLocationsResponse.class).returnResult() + .getResponseBody()) + .usingRecursiveComparison() + .isNull(); + } + } +} diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 502f133f..b44f31e9 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -2,17 +2,20 @@ import static org.assertj.core.api.Assertions.assertThatCode; +import java.util.List; import net.teumteum.core.error.ErrorResponse; import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserFixture; +import net.teumteum.user.domain.response.FriendsResponse; +import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UserMeGetResponse; -import net.teumteum.user.domain.response.*; +import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UsersGetByIdResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.List; - @DisplayName("유저 통합테스트의") class UserIntegrationTest extends IntegrationTest { @@ -125,10 +128,10 @@ void Return_my_info_if_valid_token_received() { // then Assertions.assertThat(result.expectStatus().isOk() - .expectBody(UserMeGetResponse.class) - .returnResult() - .getResponseBody()) - .usingRecursiveComparison().isEqualTo(expected); + .expectBody(UserMeGetResponse.class) + .returnResult() + .getResponseBody()) + .usingRecursiveComparison().isEqualTo(expected); } } @@ -233,7 +236,7 @@ class Withdraw_user { void Withdraw_user_info_api() { // given var me = repository.saveAndGetUser(); - repository.saveRedisDataWithExpiration(String.valueOf(me.getId()), VALID_TOKEN, DURATION); + redisRepository.saveRedisDataWithExpiration(String.valueOf(me.getId()), VALID_TOKEN, DURATION); loginContext.setUserId(me.getId()); @@ -269,9 +272,7 @@ class Register_user_card_api { @DisplayName("등록할 회원의 정보가 주어지면, 회원 정보를 저장한다.") void Register_user_info() { // given - var additionalUser = repository.saveAndGetUser(); - - var UserRegister = RequestFixture.userRegisterRequest(additionalUser); + var UserRegister = RequestFixture.userRegisterRequest(UserFixture.getIdUser()); // when var result = api.registerUserCard(VALID_TOKEN, UserRegister); @@ -301,6 +302,24 @@ void Return_400_badRequest_register_user_card() { Assertions.assertThat(responseBody) .isNotNull(); } + + @Test + @DisplayName("요청 값의 유효성 검사가 실패하면, 400 에러를 반환한다.") + void Return_400_badRequest_if_not_meet_request_condition() { + // given + var existUser = repository.saveAndGetUser(); + + var userRegister = RequestFixture.userRegisterRequestWithNoValid(existUser); + // when + var result = api.registerUserCard(VALID_TOKEN, userRegister); + + // then + ErrorResponse responseBody = result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult().getResponseBody(); + + Assertions.assertThat(responseBody).isNull(); + } } @Nested @@ -312,7 +331,7 @@ class Logout_user_api { void Logout_user() { // given var existUser = repository.saveAndGetUser(); - repository.saveRedisDataWithExpiration(String.valueOf(existUser.getId()), VALID_TOKEN, DURATION); + redisRepository.saveRedisDataWithExpiration(String.valueOf(existUser.getId()), VALID_TOKEN, DURATION); // when & then assertThatCode(() -> api.logoutUser(VALID_TOKEN)) diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index b8a03ad4..352e5e61 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -108,6 +108,22 @@ public static Meeting getCloseMeetingWithIntroduction(String introduction) { ); } + public static Meeting getOpenMeetingWithHostId(Long hostId) { + return newMeetingByBuilder(MeetingBuilder.builder() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .hostUserId(hostId) + .build() + ); + } + + public static Meeting getCloseMeetingWithHostId(Long hostId) { + return newMeetingByBuilder(MeetingBuilder.builder() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .hostUserId(hostId) + .build() + ); + } + public static Meeting newMeetingByBuilder(MeetingBuilder meetingBuilder) { return new Meeting( meetingBuilder.id, diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java index cac3e321..4d27742b 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java @@ -64,24 +64,49 @@ void Find_success_if_exists_meeting_id_input() { // then Assertions.assertThat(result) - .isPresent() - .usingRecursiveComparison() - .ignoringFields("value.createdAt", "value.updatedAt") - .isEqualTo(Optional.of(existsMeeting)); + .isPresent() + .usingRecursiveComparison() + .ignoringFields("value.createdAt", "value.updatedAt") + .isEqualTo(Optional.of(existsMeeting)); + } + } + + @Nested + @DisplayName("delete 메소드는") + class Delete_method { + + @Test + @DisplayName("모임을 삭제한 유저가 모임의 주최자이면 (hostId = userId), 모임 삭제에 성공한다.") + void Delete_success_if_exists_meeting_input() { + // given + var existsMeeting = MeetingFixture.getDefaultMeeting(); + + meetingRepository.saveAndFlush(existsMeeting); + entityManager.clear(); + + // when + meetingRepository.delete(existsMeeting); + entityManager.flush(); + entityManager.clear(); + + // then + var result = meetingRepository.findById(existsMeeting.getId()); + Assertions.assertThat(result).isEmpty(); } } @Nested @DisplayName("JPA Specification을 이용한 findAll 메소드 중") class FindAllWithSpecificationAndPageNation_method { + @Test @DisplayName("저장된 모임들을 주어진 topic을 가진 모임을 페이지 네이션을 적용해 최신순으로 조회하면, 모임들을 반환한다.") void Find_success_if_exists_meetings_topic_and_page_nation_input() { // given var createSize = 3; var expectedMeetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTopic(Topic.스터디)) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); meetingRepository.saveAllAndFlush(expectedMeetings); entityManager.clear(); @@ -95,13 +120,13 @@ void Find_success_if_exists_meetings_topic_and_page_nation_input() { // then Assertions.assertThat(result.getContent()) - .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") - .isEqualTo( - expectedMeetings.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() - ); + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo( + expectedMeetings.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() + ); } @Test @@ -110,8 +135,8 @@ void Find_success_if_exists_meetings_participant_user_id_and_page_nation_input() // given var createSize = 3; var expectedMeetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithParticipantUserId(2L)) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); meetingRepository.saveAllAndFlush(expectedMeetings); entityManager.clear(); @@ -120,18 +145,19 @@ void Find_success_if_exists_meetings_participant_user_id_and_page_nation_input() var requestParticipantUserId = 2L; // when - var spec = MeetingSpecification.withIsOpen(true).and(MeetingSpecification.withParticipantUserId(requestParticipantUserId)); + var spec = MeetingSpecification.withIsOpen(true) + .and(MeetingSpecification.withParticipantUserId(requestParticipantUserId)); var result = meetingRepository.findAll(spec, requestPageable); // then Assertions.assertThat(result.getContent()) - .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") - .isEqualTo( - expectedMeetings.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() - ); + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo( + expectedMeetings.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() + ); } @Test @@ -141,12 +167,12 @@ void Find_success_if_exists_meetings_meeting_street_and_page_nation_input() { var createSize = 3; var expectedMeetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithMainStreet("강남")) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); var existsWrongMeetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithMainStreet("판교")) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); meetingRepository.saveAll(expectedMeetings); meetingRepository.saveAllAndFlush(existsWrongMeetings); @@ -161,13 +187,13 @@ void Find_success_if_exists_meetings_meeting_street_and_page_nation_input() { // then Assertions.assertThat(result.getContent()) - .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") - .isEqualTo( - expectedMeetings.stream() - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() - ); + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo( + expectedMeetings.stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() + ); } @Test @@ -176,18 +202,18 @@ void Find_success_if_exists_meetings_search_word_page_nation_input() { // given var createSize = 3; - var existsCloseMeetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithTitle("공부팟 모집")) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); var existsMeetingsWithTitle = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTitle("공부팟 모집")) - .limit(createSize) - .toList(); + .limit(createSize) + .toList(); - var existsMeetingsWithIntroduction = Stream.generate(() -> MeetingFixture.getOpenMeetingWithIntroduction("공부하는 모임입니다.")) - .limit(createSize) - .toList(); + var existsMeetingsWithIntroduction = Stream.generate( + () -> MeetingFixture.getOpenMeetingWithIntroduction("공부하는 모임입니다.")) + .limit(createSize) + .toList(); meetingRepository.saveAll(existsCloseMeetings); meetingRepository.saveAll(existsMeetingsWithTitle); @@ -199,21 +225,21 @@ void Find_success_if_exists_meetings_search_word_page_nation_input() { // when var spec = MeetingSpecification.withSearchWordInTitle(requestSearchWord) - .or(MeetingSpecification.withSearchWordInIntroduction(requestSearchWord)) - .and(MeetingSpecification.withIsOpen(true)); + .or(MeetingSpecification.withSearchWordInIntroduction(requestSearchWord)) + .and(MeetingSpecification.withIsOpen(true)); var result = meetingRepository.findAll(spec, requestPageable); // then Assertions.assertThat(result.getContent()) - .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") - .isEqualTo( - Stream.of(existsMeetingsWithTitle, existsMeetingsWithIntroduction) - .flatMap(Collection::stream) - .sorted(Comparator.comparing(Meeting::getId).reversed()) - .toList() - ); + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo( + Stream.of(existsMeetingsWithTitle, existsMeetingsWithIntroduction) + .flatMap(Collection::stream) + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .toList() + ); } } } diff --git a/src/test/java/net/teumteum/teum_teum/UserLocationFixture.java b/src/test/java/net/teumteum/teum_teum/UserLocationFixture.java new file mode 100644 index 00000000..f896278e --- /dev/null +++ b/src/test/java/net/teumteum/teum_teum/UserLocationFixture.java @@ -0,0 +1,75 @@ +package net.teumteum.teum_teum; + +import lombok.Builder; +import net.teumteum.teum_teum.domain.request.UserLocationRequest; +import net.teumteum.teum_teum.domain.response.UserAroundLocationsResponse.UserAroundLocationResponse; + +public class UserLocationFixture { + + public static UserLocationRequest getDefaultUserLocationRequest() { + return newUserLocationRequest(UserLocationBuilder.builder().build()); + } + + public static UserAroundLocationResponse getDefaultUserAroundLocationResponse() { + return newUserAroundLocationResponse(UserAroundLocationResponseBuilder.builder().build()); + } + + public static UserLocationRequest newUserLocationRequest(UserLocationBuilder userLocationBuilder) { + return new UserLocationRequest( + userLocationBuilder.longitude, + userLocationBuilder.latitude, + userLocationBuilder.id, + userLocationBuilder.name, + userLocationBuilder.jobDetailClass, + userLocationBuilder.characterId + ); + } + + public static UserAroundLocationResponse newUserAroundLocationResponse( + UserAroundLocationResponseBuilder userAroundLocationResponseBuilder) { + return new UserAroundLocationResponse( + userAroundLocationResponseBuilder.id, + userAroundLocationResponseBuilder.name, + userAroundLocationResponseBuilder.jobDetailClass, + userAroundLocationResponseBuilder.characterId + ); + } + + @Builder + public static class UserLocationBuilder { + + @Builder.Default + private Double longitude = 120.5; + + @Builder.Default + private Double latitude = -22.3; + + @Builder.Default + private Long id = 1L; + + @Builder.Default + private String name = "John"; + + @Builder.Default + private String jobDetailClass = "backend"; + + @Builder.Default + private Long characterId = 3L; + } + + @Builder + public static class UserAroundLocationResponseBuilder { + + @Builder.Default + private Long id = 1L; + + @Builder.Default + private String name = "Mike"; + + @Builder.Default + private String jobDetailClass = "design"; + + @Builder.Default + private Long characterId = 1L; + } +} diff --git a/src/test/java/net/teumteum/unit/auth/common/SecurityValue.java b/src/test/java/net/teumteum/unit/auth/common/SecurityValue.java new file mode 100644 index 00000000..e82b8cc6 --- /dev/null +++ b/src/test/java/net/teumteum/unit/auth/common/SecurityValue.java @@ -0,0 +1,12 @@ +package net.teumteum.unit.auth.common; + +public final class SecurityValue { + + public static final String VALID_ACCESS_TOKEN = "VALID_ACCESS_TOKEN"; + public static final String INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN"; + public static final String VALID_REFRESH_TOKEN = "VALID_REFRESH_TOKEN"; + public static final String INVALID_REFRESH_TOKEN = "INVALID_REFRESH_TOKEN"; + + private SecurityValue() { + } +} diff --git a/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java b/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java index d3ecda68..cea80e23 100644 --- a/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java +++ b/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java @@ -1,6 +1,10 @@ package net.teumteum.unit.auth.controller; +import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.INVALID_REFRESH_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @@ -39,11 +43,6 @@ @DisplayName("인증 컨트롤러 단위 테스트의") public class AuthControllerTest { - private static final String VALID_ACCESS_TOKEN = "VALID_ACCESS_TOKEN"; - private static final String INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN"; - private static final String VALID_REFRESH_TOKEN = "VALID_REFRESH_TOKEN"; - private static final String INVALID_REFRESH_TOKEN = "INVALID_REFRESH_TOKEN"; - @Autowired private MockMvc mockMvc; @@ -58,7 +57,7 @@ class Reissue_jwt_api_unit { @DisplayName("유효하지 않은 access token 과 유효한 refresh token 이 주어지면, 새로운 토큰을 발급한다.") void Return_new_jwt_if_access_and_refresh_is_exist() throws Exception { // given - TokenResponse tokenResponse = new TokenResponse(INVALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); + TokenResponse tokenResponse = new TokenResponse(VALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); given(authService.reissue(any(HttpServletRequest.class))).willReturn(tokenResponse); // when & then diff --git a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java index 4312b9f3..5e194698 100644 --- a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java +++ b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java @@ -59,7 +59,7 @@ void Return_new_jwt_if_access_and_refresh_is_exist() { given(jwtService.extractRefreshToken(any(HttpServletRequest.class))).willReturn("refresh token"); - given(jwtService.getUserIdFromToken(anyString())).willReturn("1"); + given(jwtService.getUserIdFromToken(anyString())).willReturn(1L); given(jwtService.createAccessToken(anyString())).willReturn("new access token"); @@ -85,6 +85,7 @@ void Return_new_jwt_if_access_and_refresh_is_exist() { @Test @DisplayName("유효하지 않은 access token 과 유효하지 않은 refresh token 이 주어지면, 500 server 에러로 응답한다. ") void Return_500_bad_request_if_refresh_token_is_not_valid() { + // given Optional user = Optional.of(new User(1L, "oauthId", 네이버)); HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); @@ -95,14 +96,14 @@ void Return_500_bad_request_if_refresh_token_is_not_valid() { given(jwtService.validateToken(anyString())).willReturn(true); - given(jwtService.getUserIdFromToken(anyString())).willReturn("1"); + given(jwtService.getUserIdFromToken(anyString())).willReturn(1L); given(userConnector.findUserById(anyLong())).willReturn(user); given(redisService.getData(anyString())).willThrow( new IllegalArgumentException("refresh token 이 일치하지 않습니다.")); - // when + // when & then assertThatThrownBy(() -> authService.reissue(httpServletRequest)).isInstanceOf( IllegalArgumentException.class).hasMessage("refresh token 이 일치하지 않습니다."); diff --git a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java new file mode 100644 index 00000000..1b7d893e --- /dev/null +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -0,0 +1,110 @@ +package net.teumteum.unit.user.controller; + +import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import net.teumteum.core.security.SecurityConfig; +import net.teumteum.core.security.filter.JwtAuthenticationFilter; +import net.teumteum.core.security.service.JwtService; +import net.teumteum.core.security.service.RedisService; +import net.teumteum.core.security.service.SecurityService; +import net.teumteum.integration.RequestFixture; +import net.teumteum.user.controller.UserController; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserFixture; +import net.teumteum.user.domain.request.UserRegisterRequest; +import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +@WebMvcTest(value = UserController.class, + excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtAuthenticationFilter.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = RedisService.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtService.class)} +) +@WithMockUser +@DisplayName("유저 컨트롤러 단위 테스트의") +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @MockBean + private SecurityService securityService; + + private User user; + + @BeforeEach + void setUp() { + user = UserFixture.getIdUser(); + } + + @Nested + @DisplayName("유저 카드 등록 API는") + class Register_user_card_api_unit { + + @Test + @DisplayName("유효한 사용자의 등록 요청값이 주어지면, 201 Created 상태값을 반환한다.") + void Register_user_card_with_201_created() throws Exception { + // given + UserRegisterRequest request = RequestFixture.userRegisterRequest(user); + + UserRegisterResponse response = new UserRegisterResponse(1L, VALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); + + given(userService.register(any(UserRegisterRequest.class))).willReturn(response); + + // when & then + mockMvc.perform(post("/users") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(APPLICATION_JSON) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.accessToken").value(VALID_ACCESS_TOKEN)) + .andExpect(jsonPath("$.refreshToken").value(VALID_REFRESH_TOKEN)); + } + + @Test + @DisplayName("유효하지 않은 사용자의 등록 요청값이 주어지면, 400 Bad Request 상태값을 반환한다.") + void Register_user_card_with_400_bad_request() throws Exception { + // given + UserRegisterRequest request = RequestFixture.userRegisterRequestWithNoValid(user); + // when + // then + mockMvc.perform(post("/users") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(APPLICATION_JSON) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").isNotEmpty()); + } + } +} diff --git a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java new file mode 100644 index 00000000..87ffb254 --- /dev/null +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -0,0 +1,92 @@ +package net.teumteum.unit.user.service; + +import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import net.teumteum.auth.domain.response.TokenResponse; +import net.teumteum.core.security.service.JwtService; +import net.teumteum.core.security.service.RedisService; +import net.teumteum.integration.RequestFixture; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserFixture; +import net.teumteum.user.domain.UserRepository; +import net.teumteum.user.domain.request.UserRegisterRequest; +import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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; + +@ExtendWith(MockitoExtension.class) +@DisplayName("유저 서비스 단위 테스트의") +public class UserServiceTest { + + @InjectMocks + UserService userService; + + @Mock + UserRepository userRepository; + + @Mock + RedisService redisService; + + @Mock + JwtService jwtService; + + private User user; + + @BeforeEach + void setUp() { + user = UserFixture.getIdUser(); + } + + @Nested + @DisplayName("유저 카드 등록 API는") + class Register_user_card_api_unit { + + @Test + @DisplayName("유효한 유저의 요청 값이 들어오는 경우, 정상적으로 유저 카드를 등록한다.") + void If_valid_user_request_register_user_card() { + // given + UserRegisterRequest request = RequestFixture.userRegisterRequest(user); + TokenResponse tokenResponse = new TokenResponse(VALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); + + UserRegisterResponse response = UserRegisterResponse.of(1L, tokenResponse); + + given(userRepository.save(any(User.class))).willReturn(user); + + given(jwtService.createServiceToken(any(User.class))).willReturn(tokenResponse); + + // when + UserRegisterResponse result = userService.register(request); + + // then + assertThat(response.id()).isEqualTo(1); + assertThat(response.accessToken()).isNotNull(); + assertThat(response.refreshToken()).isNotNull(); + } + + @Test + @DisplayName("사용자가 이미 존재하면, 카드 등록을 실패한다.") + void If_user_already_exist_register_user_card_fail() { + // given + UserRegisterRequest request = RequestFixture.userRegisterRequestWithFail(user); + + given(userRepository.findByAuthenticatedAndOAuthId(any(), any())) + .willThrow(new IllegalArgumentException("일치하는 user 가 이미 존재합니다.")); + + assertThatThrownBy(() -> userService.register(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("일치하는 user 가 이미 존재합니다."); + } + } +} diff --git a/src/test/java/net/teumteum/user/domain/UserFixture.java b/src/test/java/net/teumteum/user/domain/UserFixture.java index 5365cb14..ab8faaaa 100644 --- a/src/test/java/net/teumteum/user/domain/UserFixture.java +++ b/src/test/java/net/teumteum/user/domain/UserFixture.java @@ -1,31 +1,30 @@ package net.teumteum.user.domain; -import lombok.Builder; +import static net.teumteum.core.security.Authenticated.네이버; import java.util.List; import java.util.Set; import java.util.UUID; - -import static net.teumteum.core.security.Authenticated.네이버; +import lombok.Builder; public class UserFixture { public static User getNullIdUser() { return newUserByBuilder(UserBuilder.builder() - .id(null) - .build()); + .id(null) + .build()); } public static User getIdUser() { return newUserByBuilder(UserBuilder.builder() - .id(1L) - .build()); + .id(1L) + .build()); } public static User getUserWithId(Long id) { return newUserByBuilder(UserBuilder.builder() - .id(id) - .build()); + .id(id) + .build()); } public static User getDefaultUser() { @@ -34,21 +33,21 @@ public static User getDefaultUser() { public static User newUserByBuilder(UserBuilder userBuilder) { return new User( - userBuilder.id, - userBuilder.name, - userBuilder.birth, - userBuilder.characterId, - userBuilder.mannerTemperature, - userBuilder.oauth, - userBuilder.roleType, - userBuilder.activityArea, - userBuilder.mbti, - userBuilder.status, - userBuilder.goal, - userBuilder.job, - userBuilder.interests, - userBuilder.terms, - Set.of() + userBuilder.id, + userBuilder.name, + userBuilder.birth, + userBuilder.characterId, + userBuilder.mannerTemperature, + userBuilder.oauth, + userBuilder.roleType, + userBuilder.activityArea, + userBuilder.mbti, + userBuilder.status, + userBuilder.goal, + userBuilder.job, + userBuilder.interests, + userBuilder.terms, + Set.of() ); } @@ -60,7 +59,7 @@ public static class UserBuilder { @Builder.Default private String name = "Jennifer"; @Builder.Default - private String birth = "2000.02.05"; + private String birth = "20000205"; @Builder.Default private Long characterId = 1L; @Builder.Default @@ -70,7 +69,7 @@ public static class UserBuilder { @Builder.Default private RoleType roleType = RoleType.ROLE_USER; @Builder.Default - private ActivityArea activityArea = new ActivityArea("서울", List.of("강남", "홍대")); + private String activityArea = "서울 강남"; @Builder.Default private String mbti = "ESFP"; @Builder.Default @@ -81,7 +80,7 @@ public static class UserBuilder { private Job job = new Job("netflix", true, "developer", "backend"); @Builder.Default private List interests = List.of( - "game", "sleep", "Eating delicious food" + "game", "sleep", "Eating delicious food" ); @Builder.Default private Terms terms = new Terms(true, true); diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index d9752b2a..6ed18368 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -27,10 +27,14 @@ spring.security.oauth2.client.provider.kakao.user-name-attribute=https://kauth.k spring.cloud.aws.credentials.access-key=12345678910 spring.cloud.aws.credentials.secret-key=12345678910 spring.cloud.aws.region.static=ap-northeast-2 -spring.cloud.aws.s3.bucket: test-bucket +spring.cloud.aws.s3.bucket=test-bucket ### Redis ### spring.data.redis.host=localhost -spring.data.redis.port=6378 +spring.data.redis.port=6379 ### JWT ### jwt.bearer=Bearer -jwt.secret=secret +jwt.secret=a2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRvsdsadwsadasdwSDSAweasDSadwXJsecretsecretsecretsecretsecreetsecret +jwt.access.header=Authorization +jwt.access.expiration=3600000 +jwt.refresh.expiration=3600000 +jwt.refresh.header=Authorization-refresh diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 550c5d84..1304b85a 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,6 +1,6 @@ create table if not exists users ( - id bigint not null auto_increment, + id bigint not null auto_increment, certificated boolean, manner_temperature integer, mbti varchar(4), @@ -8,18 +8,18 @@ create table if not exists users birth varchar(10), name varchar(10), goal varchar(50), - oauth_id varchar(255) not null unique, + oauth_id varchar(255) not null unique, authenticated enum ('카카오','네이버') not null, role_type enum ('ROLE_USER','ROLE_ADMIN'), - city varchar(255), + activity_area varchar(255), detail_job_class varchar(255), job_class varchar(255), job_name varchar(255), status enum ('직장인','학생','취업준비생'), - terms_of_service boolean not null, - privacy_policy boolean not null, - created_at timestamp(6) not null, - updated_at timestamp(6) not null, + terms_of_service boolean not null, + privacy_policy boolean not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, primary key (id) ); @@ -30,12 +30,6 @@ create table if not exists users_interests foreign key (users_id) references users (id) ); -create table if not exists users_street -( - users_id bigint not null, - street varchar(255), - foreign key (users_id) references users (id) -); create table if not exists meeting (