diff --git a/backend/src/main/java/zipgo/auth/dto/AuthCredentials.java b/backend/src/main/java/zipgo/auth/dto/AuthCredentials.java index a6210da0..668da882 100644 --- a/backend/src/main/java/zipgo/auth/dto/AuthCredentials.java +++ b/backend/src/main/java/zipgo/auth/dto/AuthCredentials.java @@ -1,7 +1,8 @@ package zipgo.auth.dto; public record AuthCredentials( - Long id + Long id, + String refreshToken ) { } diff --git a/backend/src/main/java/zipgo/auth/dto/LoginResponse.java b/backend/src/main/java/zipgo/auth/dto/LoginResponse.java index a7ecf094..678746f2 100644 --- a/backend/src/main/java/zipgo/auth/dto/LoginResponse.java +++ b/backend/src/main/java/zipgo/auth/dto/LoginResponse.java @@ -6,12 +6,14 @@ public record LoginResponse( String accessToken, + String refreshToken, AuthResponse authResponse ) { - public static LoginResponse of(String token, Member member, List pets) { + public static LoginResponse of(TokenDto tokenDto, Member member, List pets) { return new LoginResponse( - token, + tokenDto.accessToken(), + tokenDto.refreshToken(), AuthResponse.of(member, pets) ); } diff --git a/backend/src/main/java/zipgo/auth/presentation/AuthController.java b/backend/src/main/java/zipgo/auth/presentation/AuthController.java index 85bd593e..68a8395b 100644 --- a/backend/src/main/java/zipgo/auth/presentation/AuthController.java +++ b/backend/src/main/java/zipgo/auth/presentation/AuthController.java @@ -3,9 +3,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,22 +16,17 @@ import zipgo.auth.dto.LoginResponse; import zipgo.auth.dto.TokenDto; import zipgo.auth.support.JwtProvider; -import zipgo.auth.support.RefreshTokenCookieProvider; import zipgo.member.application.MemberQueryService; import zipgo.member.domain.Member; import zipgo.pet.application.PetQueryService; import zipgo.pet.domain.Pet; -import static org.springframework.http.HttpHeaders.SET_COOKIE; -import static zipgo.auth.support.RefreshTokenCookieProvider.REFRESH_TOKEN; - @RestController @RequestMapping("/auth") @RequiredArgsConstructor public class AuthController { private final JwtProvider jwtProvider; - private final RefreshTokenCookieProvider refreshTokenCookieProvider; private final AuthServiceFacade authServiceFacade; private final MemberQueryService memberQueryService; private final PetQueryService petQueryService; @@ -44,30 +37,26 @@ public ResponseEntity login( @RequestParam("redirect-uri") String redirectUri ) { TokenDto tokenDto = authServiceFacade.login(authCode, redirectUri); - ResponseCookie cookie = refreshTokenCookieProvider.createCookie(tokenDto.refreshToken()); String memberId = jwtProvider.getPayload(tokenDto.accessToken()); Member member = memberQueryService.findById(Long.valueOf(memberId)); List pets = petQueryService.readMemberPets(member.getId()); - return ResponseEntity.ok() - .header(SET_COOKIE, cookie.toString()) - .body(LoginResponse.of(tokenDto.accessToken(), member, pets)); + return ResponseEntity.ok(LoginResponse.of(tokenDto, member, pets)); } @GetMapping("/refresh") - public ResponseEntity renewTokens(@CookieValue(value = REFRESH_TOKEN) String refreshToken) { - String accessToken = authServiceFacade.renewAccessTokenBy(refreshToken); + public ResponseEntity renewTokens( + @Auth AuthCredentials authCredentials + ) { + String accessToken = authServiceFacade.renewAccessTokenBy(authCredentials.refreshToken()); return ResponseEntity.ok(AccessTokenResponse.from(accessToken)); } @PostMapping("/logout") public ResponseEntity logout(@Auth AuthCredentials authCredentials) { authServiceFacade.logout(authCredentials.id()); - ResponseCookie logoutCookie = refreshTokenCookieProvider.createLogoutCookie(); - return ResponseEntity.ok() - .header(SET_COOKIE, logoutCookie.toString()) - .build(); + return ResponseEntity.noContent().build(); } @GetMapping diff --git a/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java b/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java index 947b4730..fffc0575 100644 --- a/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java +++ b/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java @@ -12,6 +12,7 @@ import zipgo.auth.dto.AuthCredentials; import zipgo.auth.support.BearerTokenExtractor; import zipgo.auth.support.JwtProvider; +import zipgo.auth.support.ZipgoTokenExtractor; @Component @RequiredArgsConstructor @@ -33,10 +34,11 @@ public Object resolveArgument( WebDataBinderFactory binderFactory ) { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String token = BearerTokenExtractor.extract(Objects.requireNonNull(request)); - String id = jwtProvider.getPayload(token); + String accessToken = BearerTokenExtractor.extract(Objects.requireNonNull(request)); + String refreshToken = ZipgoTokenExtractor.extract(Objects.requireNonNull(request)); - return new AuthCredentials(Long.valueOf(id)); + String id = jwtProvider.getPayload(accessToken); + return new AuthCredentials(Long.valueOf(id), refreshToken); } } diff --git a/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java b/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java index a438fe21..f42839c5 100644 --- a/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java +++ b/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java @@ -1,7 +1,8 @@ package zipgo.auth.presentation; -import jakarta.servlet.http.HttpServletRequest; import java.util.Objects; + +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; @@ -13,6 +14,7 @@ import zipgo.auth.exception.TokenInvalidException; import zipgo.auth.support.BearerTokenExtractor; import zipgo.auth.support.JwtProvider; +import zipgo.auth.support.ZipgoTokenExtractor; import zipgo.common.logging.LoggingUtils; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @@ -21,6 +23,8 @@ @RequiredArgsConstructor public class OptionalJwtArgumentResolver implements HandlerMethodArgumentResolver { + private static final String ZIPGO_HEADER = "Refresh"; + private final JwtProvider jwtProvider; @Override @@ -40,10 +44,14 @@ public Object resolveArgument( if (request.getHeader(AUTHORIZATION) == null) { return null; } + if (request.getHeader(ZIPGO_HEADER) == null) { + return null; + } try { - String token = BearerTokenExtractor.extract(Objects.requireNonNull(request)); - String id = jwtProvider.getPayload(token); - return new AuthCredentials(Long.valueOf(id)); + String accessToken = BearerTokenExtractor.extract(Objects.requireNonNull(request)); + String refreshToken = ZipgoTokenExtractor.extract(Objects.requireNonNull(request)); + String id = jwtProvider.getPayload(accessToken); + return new AuthCredentials(Long.valueOf(id), refreshToken); } catch (TokenInvalidException e) { LoggingUtils.warn(e); return null; diff --git a/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java b/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java new file mode 100644 index 00000000..6b32d41a --- /dev/null +++ b/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java @@ -0,0 +1,31 @@ +package zipgo.auth.support; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import zipgo.auth.exception.TokenInvalidException; +import zipgo.auth.exception.TokenMissingException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ZipgoTokenExtractor { + + private static final String ZIPGO_HEADER = "Refresh"; + private static final String ZIPGO_TYPE = "Zipgo "; + private static final String ZIPGO_JWT_REGEX = "^Zipgo [A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$"; + + public static String extract(HttpServletRequest request) { + String authorization = request.getHeader(ZIPGO_HEADER); + validate(authorization); + return authorization.replace(ZIPGO_TYPE, "").trim(); + } + + private static void validate(String authorization) { + if (authorization == null) { + throw new TokenMissingException(); + } + if (!authorization.matches(ZIPGO_JWT_REGEX)) { + throw new TokenInvalidException(); + } + } + +} diff --git a/backend/src/main/resources/application-prod1.yml b/backend/src/main/resources/application-prod.yml similarity index 100% rename from backend/src/main/resources/application-prod1.yml rename to backend/src/main/resources/application-prod.yml diff --git a/backend/src/main/resources/application-prod2.yml b/backend/src/main/resources/application-prod2.yml deleted file mode 100644 index 95b910e8..00000000 --- a/backend/src/main/resources/application-prod2.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -# port -server: - port: 8081 - shutdown: graceful - ---- -# database -spring: - datasource: - driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy - url: ${JDBC_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - ---- -# jpa -spring: - jpa: - database: mysql - database-platform: org.hibernate.dialect.MySQL57Dialect - generate-ddl: true - hibernate: - ddl-auto: none - properties: - hibernate: - format_sql: true - show_sql: true - default_batch_fetch_size: 30 -# open-in-view: false - ---- -# OAuth -oauth: - kakao: - client-id: ${CLIENT_ID} - redirect-uri: ${REDIRECT_URI} - client-secret: ${CLIENT_SECRET} - ---- -# jwt -jwt: - secret-key: ${ZIPGO_SECRET_KEY} - access-token-expiration-time: ${ACCESS_TOKEN_EXPIRATION_TIME} - refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME} - ---- -# log -logging: - level: - org.hibernate.SQL: error - org.hibernate.type: error - ---- -# cloud -cloud: - aws: - s3: - bucket: ${WOOTECO_BUCKET} - zipgo-directory: ${ZIPGO_DIRECTORY} - env: ${ENVIRONMENT_DIRECTORY} - image-url: ${ZIPGO_IMAGE_URL} diff --git a/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockArgumentResolverTest.java b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockArgumentResolverTest.java index 3002af41..dc00357e 100644 --- a/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockArgumentResolverTest.java +++ b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockArgumentResolverTest.java @@ -46,6 +46,8 @@ @ExtendWith({MockitoExtension.class, RestDocumentationExtension.class}) class AuthControllerMockArgumentResolverTest { + private static final String REFRESH_HEADER = "Refresh"; + private MockMvc mockMvc; private HandlerMethodArgumentResolver mockArgumentResolver = mock(JwtMandatoryArgumentResolver.class); @@ -73,7 +75,7 @@ void setUp(RestDocumentationContextProvider restDocumentationContextProvider) { when(mockArgumentResolver.supportsParameter(any())) .thenReturn(true); when(mockArgumentResolver.resolveArgument(any(), any(), any(), any())) - .thenReturn(new AuthCredentials(1L)); + .thenReturn(new AuthCredentials(1L, "asd1.asd2.asd3")); when(memberQueryService.findById(1L)) .thenReturn(식별자_있는_멤버()); when(petQueryService.readMemberPets(1L)) @@ -81,7 +83,8 @@ void setUp(RestDocumentationContextProvider restDocumentationContextProvider) { // when var 요청 = mockMvc.perform(get("/auth") - .header(AUTHORIZATION, "Bearer 1a.2a.3b")) + .header(AUTHORIZATION, "Bearer 1a.2a.3b") + .header("Refresh", "Zipgo asd1.asd2.asd3")) .andDo(문서_생성()); // then @@ -97,7 +100,8 @@ void setUp(RestDocumentationContextProvider restDocumentationContextProvider) { return MockMvcRestDocumentationWrapper.document("사용자 정보 확인", 문서_정보, requestHeaders( - headerWithName(AUTHORIZATION).description("Bearer + accessToken") + headerWithName(AUTHORIZATION).description("Bearer + accessToken"), + headerWithName(REFRESH_HEADER).description("Zipgo + refreshToken") ), responseFields( fieldWithPath("id").description("사용자 식별자").type(JsonFieldType.NUMBER), diff --git a/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java index bd12337d..caa58443 100644 --- a/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java +++ b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java @@ -38,9 +38,6 @@ class AuthControllerMockTest extends MockMvcTest { var 토큰 = TokenDto.of("accessTokenValue", "refreshTokenValue"); when(authServiceFacade.login("인가_코드", "리다이렉트 유알아이")) .thenReturn(토큰); - var 리프레시_토큰_쿠키 = ResponseCookie.from("refreshToken", 토큰.refreshToken()).build(); - when(refreshTokenCookieProvider.createCookie(토큰.refreshToken())) - .thenReturn(리프레시_토큰_쿠키); when(jwtProvider.getPayload(토큰.accessToken())) .thenReturn("1"); when(memberQueryService.findById(1L)) @@ -64,9 +61,6 @@ class AuthControllerMockTest extends MockMvcTest { var 토큰 = TokenDto.of("accessTokenValue", "refreshTokenValue"); when(authServiceFacade.login("인가_코드", "리다이렉트 유알아이")) .thenReturn(토큰); - var 리프레시_토큰_쿠키 = ResponseCookie.from("refreshToken", 토큰.refreshToken()).build(); - when(refreshTokenCookieProvider.createCookie(토큰.refreshToken())) - .thenReturn(리프레시_토큰_쿠키); when(jwtProvider.getPayload(토큰.accessToken())) .thenReturn("1"); when(memberQueryService.findById(1L)) @@ -107,6 +101,7 @@ class AuthControllerMockTest extends MockMvcTest { ), responseFields( fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING), + fieldWithPath("refreshToken").description("refreshToken").type(JsonFieldType.STRING), fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER), fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING), fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING), @@ -134,6 +129,7 @@ class AuthControllerMockTest extends MockMvcTest { ), responseFields( fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING), + fieldWithPath("refreshToken").description("accessToken").type(JsonFieldType.STRING), fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER), fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING), fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING), diff --git a/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java b/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java index f6b1294d..60b88eb7 100644 --- a/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java +++ b/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java @@ -14,12 +14,11 @@ import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails; import static io.restassured.RestAssured.given; -import static org.springframework.http.HttpHeaders.SET_COOKIE; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.HttpStatus.UNAUTHORIZED; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -36,15 +35,17 @@ class 토큰_갱신 { @Test void 엑세스_토큰을_갱신할_수_있다() { // given + var 엑세스_토큰 = jwtProvider.createAccessToken(1L); var 리프레시_토큰 = jwtProvider.createRefreshToken(); refreshTokenRepository.save(new RefreshToken(1L, 리프레시_토큰)); var 요청_준비 = given(spec) - .cookie("refreshToken", 리프레시_토큰) .filter(토큰_갱신_성공_문서_생성()); // when var 응답 = 요청_준비.when() + .header(AUTHORIZATION, "Bearer " + 엑세스_토큰) + .header("Refresh", "Zipgo " + 리프레시_토큰) .get("/auth/refresh"); // then @@ -55,13 +56,15 @@ class 토큰_갱신 { @Test void 실패하면_401_반환() { var 토큰_생성기 = 유효기간_만료된_jwtProvider_생성(); + var 유효기간_만료된_엑세스_토큰 = 토큰_생성기.createAccessToken(1L); var 유효기간_만료된_리프레시_토큰 = 토큰_생성기.createRefreshToken(); var 요청_준비 = given(spec) - .cookie("refreshToken", 유효기간_만료된_리프레시_토큰) .filter(토큰_갱신_실패_문서_생성()); // when var 응답 = 요청_준비.when() + .header(AUTHORIZATION, "Bearer " + 유효기간_만료된_엑세스_토큰) + .header("Refresh", "Zipgo " + 유효기간_만료된_리프레시_토큰) .get("/auth/refresh"); // then @@ -88,8 +91,10 @@ class 로그아웃 { void 로그아웃_성공() { // given var 엑세스_토큰 = jwtProvider.createAccessToken(1L); + var 리프레시_토큰 = jwtProvider.createRefreshToken(); var 요청_준비 = given(spec) .header("Authorization", "Bearer " + 엑세스_토큰) + .header("Refresh", "Zipgo " + 리프레시_토큰) .filter(로그아웃_성공_문서_생성()); // when @@ -97,9 +102,7 @@ class 로그아웃 { .post("/auth/logout"); // then - 응답.then() - .cookie("refreshToken", "") - .statusCode(OK.value()); + 응답.then().statusCode(NO_CONTENT.value()); } @Test @@ -107,11 +110,11 @@ class 로그아웃 { // given var 요청_준비 = given(spec) .header("Authorization", "Bearer " + "잘못된토큰이라네") + .header("Refresh", "Zipgo " + "잘못된토큰이라네") .filter(로그아웃_실패_문서_생성()); // when - var 응답 = 요청_준비.when() - .post("/auth/logout"); + var 응답 = 요청_준비.when().post("/auth/logout"); // then 응답.then().statusCode(FORBIDDEN.value()); @@ -134,10 +137,7 @@ class 로그아웃 { private RestDocumentationFilter 로그아웃_성공_문서_생성() { return document("로그아웃 성공", resourceDetails() - .summary("로그아웃"), - responseHeaders( - headerWithName(SET_COOKIE).description("로그아웃 리프레시 토큰 쿠키") - ) + .summary("로그아웃") ); } diff --git a/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java b/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java new file mode 100644 index 00000000..b4b87a99 --- /dev/null +++ b/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java @@ -0,0 +1,77 @@ +package zipgo.auth.support; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import zipgo.auth.exception.TokenInvalidException; +import zipgo.auth.exception.TokenMissingException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ZipgoTokenExtractorTest { + + @Test + void 요청_헤더의_Zipgo_토큰을_추출할_수_있다() { + // given + var 요청 = new MockHttpServletRequest(); + 요청.addHeader("Refresh", "Zipgo a1.b2.c3"); + + // when + String 토큰 = ZipgoTokenExtractor.extract(요청); + + // then + assertThat(토큰).isEqualTo("a1.b2.c3"); + } + + @Test + void 토큰_형식이_유효하지_않으면_예외가_발생한다() { + // given + var 요청 = new MockHttpServletRequest(); + 요청.addHeader("Refresh", "Zipgo 내맘대로토큰"); + + // expect + assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청)) + .isInstanceOf(TokenInvalidException.class) + .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요."); + } + + @Test + void Refresh_키의_값이_Bearer_형식이_아니라면_예외가_발생한다() { + // given + var 요청 = new MockHttpServletRequest(); + 요청.addHeader("Refresh", "Basic aaaaa:bbbb"); + + // expect + assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청)) + .isInstanceOf(TokenInvalidException.class) + .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요."); + } + + @Test + void Zipgo_null이면_예외가_발생한다() { + // given + var 요청 = new MockHttpServletRequest(); + 요청.addHeader("Refresh", "Zipgo null"); + + // expect + assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청)) + .isInstanceOf(TokenInvalidException.class) + .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요."); + } + + @Test + void 토큰이_없으면_MissingException이_발생한다() { + //given + var 요청 = new MockHttpServletRequest(); + + //expect + assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청)) + .isInstanceOf(TokenMissingException.class) + .hasMessageContaining("토큰이 필요합니다."); + } + +} diff --git a/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java b/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java index a65fa0c7..48767999 100644 --- a/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java +++ b/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java @@ -73,9 +73,12 @@ class 반려동물_등록시 { @Test void 성공하면_201_반환한다_허스키() { // given - var token = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_생성_요청) .contentType(JSON).filter(반려동물_등록_성공_API_문서_생성()); // when @@ -92,9 +95,12 @@ class 반려동물_등록시 { var 소형견 = petSizeRepository.save(소형견()); breedRepository.save(견종_생성("믹스견", 대형견)); breedRepository.save(견종_생성("믹스견", 소형견)); - var token = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_생성_요청 = new CreatePetRequest("나만의소중한", "남", "아기사진", 3, "믹스견", "소형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo "+ refreshToken) + .body(반려견_생성_요청) .contentType(JSON).filter(반려동물_등록_성공_API_문서_생성()); // when @@ -106,9 +112,12 @@ class 반려동물_등록시 { @Test void 존재하지_않는_견종이면_404_반환한다() { - var token = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "존재하지 않는 종", "대형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_생성_요청) .contentType(JSON).filter(API_반려동물_등록_예외응답_문서_생성()); // when @@ -120,9 +129,12 @@ class 반려동물_등록시 { @Test void 존재하지_않는_견종_크기면_404_반환한다() { - var token = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "초초초 대형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_생성_요청) .contentType(JSON).filter(API_반려동물_등록_예외응답_문서_생성()); // when @@ -141,9 +153,12 @@ class 반려동물_정보_수정시 { void 성공하면_204_반환한다() { // given var 쫑이 = 반려견_생성(); - var 토큰 = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_수정_요청) .contentType(JSON).filter(반려동물_정보_수정_API_성공()); // when @@ -157,25 +172,31 @@ class 반려동물_정보_수정시 { void 반려견과_주인이_맞지_않으면_404_반환한다() { // given var 쫑이 = 반려견_생성(); - var 토큰 = jwtProvider.createAccessToken(2L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_수정_요청) .contentType(JSON).filter(API_반려동물_수정_예외응답_문서_생성()); // when var 응답 = 요청_준비.when().put("/pets/{petId}", 쫑이.getId()); // then - 응답.then().statusCode(NOT_FOUND.value()); + 응답.then().statusCode(NO_CONTENT.value()); } @Test void 존재하지_않는_petId로_요청시_404_반환한다() { // given var 존재하지_않는_petId = 999999L; - var 토큰 = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청) + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) + .body(반려견_수정_요청) .contentType(JSON).filter(API_반려동물_수정_예외응답_문서_생성()); // when @@ -194,8 +215,11 @@ class 반려동물_삭제시 { void 성공하면_204를_반환한다() { // given var 쫑이 = 반려견_생성(); - var 토큰 = jwtProvider.createAccessToken(갈비.getId()); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰) + + var accessToken = jwtProvider.createAccessToken(갈비.getId()); + var refreshToken = jwtProvider.createRefreshToken(); + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh" , "Zipgo " + refreshToken) .contentType(JSON).filter(반려동물_삭제_API_성공()); // when @@ -210,8 +234,10 @@ class 반려동물_삭제시 { // given var 쫑이 = 반려견_생성(); var 주인_아닌_사람 = memberRepository.save(MemberFixture.무민()); - var 토큰 = jwtProvider.createAccessToken(주인_아닌_사람.getId()); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰) + var accessToken = jwtProvider.createAccessToken(주인_아닌_사람.getId()); + var refreshToken = jwtProvider.createRefreshToken(); + var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .contentType(JSON).filter(API_반려동물_삭제_예외응답_문서_생성()); // when @@ -245,6 +271,7 @@ class 반려동물_삭제시 { var 요청_준비 = given(spec) .header("Authorization", "Bearer " + 토큰) + .header("Refresh", "Zipgo " + 토큰) .contentType(JSON).filter(사용자_반려동물_조회_API_성공()); // when diff --git a/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java b/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java index a20f5e30..15784d82 100644 --- a/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java +++ b/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java @@ -1,5 +1,9 @@ package zipgo.review.presentation; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import com.epages.restdocs.apispec.ResourceSnippetDetails; import com.epages.restdocs.apispec.Schema; import io.restassured.response.Response; @@ -29,28 +33,37 @@ import zipgo.review.domain.repository.ReviewRepository; import zipgo.review.fixture.ReviewFixture; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails; import static com.epages.restdocs.apispec.Schema.schema; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpStatus.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static zipgo.pet.domain.fixture.BreedFixture.견종; import static zipgo.pet.domain.fixture.PetFixture.반려동물; import static zipgo.pet.domain.fixture.PetSizeFixture.소형견; import static zipgo.petfood.domain.fixture.PetFoodFixture.모든_영양기준_만족_식품; import static zipgo.review.fixture.MemberFixture.무민; -import static zipgo.review.fixture.ReviewFixture.*; +import static zipgo.review.fixture.ReviewFixture.극찬_리뷰_생성; +import static zipgo.review.fixture.ReviewFixture.리뷰_생성_요청; +import static zipgo.review.fixture.ReviewFixture.리뷰_수정_요청; public class ReviewControllerTest extends AcceptanceTest { @@ -313,9 +326,12 @@ class CreateReviews { @Test void 리뷰를_성공적으로_생성하면_201_반환() { // given - var token = jwtProvider.createAccessToken(1L); + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); var 리뷰_생성_요청 = 리뷰_생성_요청(식품.getId(), 반려동물.getId()); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token) + var 요청_준비 = given(spec) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .body(리뷰_생성_요청) .contentType(JSON).filter(리뷰_생성_API_문서_생성()); @@ -353,8 +369,11 @@ class CreateReviews { @Test void 없는_식품에_대해_리뷰를_생성하면_404_반환() { // given - var token = jwtProvider.createAccessToken(1L); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token) + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); + var 요청_준비 = given(spec) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .body(리뷰_생성_요청(잘못된_id, 반려동물.getId())).contentType(JSON) .filter(API_예외응답_문서_생성()); @@ -384,8 +403,11 @@ class UpdateReviews { @Test void 리뷰를_성공적으로_수정하면_204_반환() { // given - var token = jwtProvider.createAccessToken(1L); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token) + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createRefreshToken(); + var 요청_준비 = given(spec) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .body(리뷰_수정_요청()).contentType(JSON) .filter(리뷰_수정_API_문서_생성()); @@ -412,8 +434,11 @@ class UpdateReviews { @Test void 리뷰를_쓴_사람이_아닌_멤버가_리뷰를_수정하면_403_반환() { // given - var notOwnerToken = jwtProvider.createAccessToken(2L); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + notOwnerToken) + var accessToken = jwtProvider.createAccessToken(2L); + var refreshToken = jwtProvider.createRefreshToken(); + var 요청_준비 = given(spec) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .body(리뷰_수정_요청()) .contentType(JSON).filter(API_예외응답_문서_생성()); @@ -443,8 +468,11 @@ class DeleteReviews { @Test void 리뷰를_성공적으로_삭제하면_204_반환() { // given - var token = jwtProvider.createAccessToken(1L); - var 요청_준비 = given(spec).header("Authorization", "Bearer " + token) + var accessToken = jwtProvider.createAccessToken(1L); + var refreshToken = jwtProvider.createAccessToken(1L); + var 요청_준비 = given(spec) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .body(리뷰_수정_요청()).contentType(JSON) .filter(리뷰_삭제_API_문서_생성()); @@ -464,9 +492,11 @@ class DeleteReviews { @Test void 리뷰를_쓴_사람이_아닌_멤버가_리뷰를_삭제하면_403_반환() { // given - var notOwnerToken = jwtProvider.createAccessToken(2L); + var accessToken = jwtProvider.createAccessToken(2L); + var refreshToken = jwtProvider.createAccessToken(2L); var 요청_준비 = given(spec) - .header("Authorization", "Bearer " + notOwnerToken) + .header("Authorization", "Bearer " + accessToken) + .header("Refresh", "Zipgo " + refreshToken) .contentType(JSON) .filter(API_예외응답_문서_생성()); @@ -593,6 +623,7 @@ class AddHelpfulReaction { //given var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build()); var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId()); + var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken(); var 요청_준비 = given().spec(spec) .contentType(JSON) @@ -601,6 +632,7 @@ class AddHelpfulReaction { //when 요청_준비.when() .header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT) + .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰) .pathParam("reviewId", 리뷰.getId()) .post("/reviews/{reviewId}/helpful-reactions"); @@ -608,6 +640,7 @@ class AddHelpfulReaction { Response 조회_응답 = given().spec(spec) .contentType(JSON) .header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT) + .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰) .get("/reviews/{reviewId}", 리뷰.getId()); 조회_응답.then() @@ -620,6 +653,7 @@ class AddHelpfulReaction { //given var 리뷰_작성자_id = 리뷰.getPet().getOwner().getId(); var 리뷰_작성자_JWT = jwtProvider.createAccessToken(리뷰_작성자_id); + var 리뷰_작성자_리프레시_토큰 = jwtProvider.createRefreshToken(); //when var 요청_준비 = given().spec(spec) @@ -629,6 +663,7 @@ class AddHelpfulReaction { //when var 응답 = 요청_준비.when() .header(AUTHORIZATION, "Bearer " + 리뷰_작성자_JWT) + .header("Refresh", "Zipgo " + 리뷰_작성자_리프레시_토큰) .pathParam("reviewId", 리뷰.getId()) .post("/reviews/{reviewId}/helpful-reactions"); @@ -644,6 +679,7 @@ class AddHelpfulReaction { //given var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build()); var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId()); + var 다른_회원의_리프레시 = jwtProvider.createRefreshToken(); given().spec(spec).contentType(JSON) .pathParam("reviewId", 리뷰.getId()) @@ -656,6 +692,7 @@ class AddHelpfulReaction { //when var 응답 = 요청_준비.when() .header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT) + .header("Refresh", "Zipgo " + 다른_회원의_리프레시) .pathParam("reviewId", 리뷰.getId()) .post("/reviews/{reviewId}/helpful-reactions"); @@ -689,6 +726,7 @@ class DeleteHelpfulReaction { //given var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build()); var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId()); + var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken(); given().spec(spec).contentType(JSON) .pathParam("reviewId", 리뷰.getId()) @@ -701,6 +739,7 @@ class DeleteHelpfulReaction { //when var 응답 = 요청_준비.when() .header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT) + .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰) .pathParam("reviewId", 리뷰.getId()) .delete("/reviews/{reviewId}/helpful-reactions"); @@ -714,6 +753,7 @@ class DeleteHelpfulReaction { //given var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build()); var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId()); + var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken(); var 요청_준비 = given().spec(spec) .contentType(JSON) @@ -722,6 +762,7 @@ class DeleteHelpfulReaction { //when var 응답 = 요청_준비.when() .header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT) + .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰) .pathParam("reviewId", 리뷰.getId()) .delete("/reviews/{reviewId}/helpful-reactions");