From c4a885a69e7d3c1a4b87dfa7ffbd5b2d31b8af51 Mon Sep 17 00:00:00 2001 From: zangsu Date: Wed, 18 Dec 2024 16:29:05 +0900 Subject: [PATCH 01/16] =?UTF-8?q?refactor:=20AuthorizationHeader=20?= =?UTF-8?q?=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/AuthArgumentResolver.java | 16 +++- .../configuration/AuthWebConfiguration.java | 4 +- .../auth/controller/AuthController.java | 20 ++++- .../AuthorizationHeaderCredentialManager.java | 2 + .../src/test/java/codezap/auth/AuthTest.java | 76 +++++++++++++++++++ .../AuthArgumentResolverTest.java | 13 +++- .../src/test/java/codezap/global/MvcTest.java | 26 +++++++ 7 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 backend/src/test/java/codezap/auth/AuthTest.java create mode 100644 backend/src/test/java/codezap/global/MvcTest.java diff --git a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java index 36f90531e..79b10a38b 100644 --- a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java +++ b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java @@ -3,8 +3,11 @@ import codezap.auth.dto.Credential; import codezap.auth.manager.CredentialManager; import codezap.auth.provider.CredentialProvider; +import codezap.global.exception.CodeZapException; +import codezap.global.exception.ErrorCode; import codezap.member.domain.Member; import jakarta.servlet.http.HttpServletRequest; +import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; @@ -17,7 +20,7 @@ @RequiredArgsConstructor public class AuthArgumentResolver implements HandlerMethodArgumentResolver { - private final CredentialManager credentialManager; + private final List credentialManagers; private final CredentialProvider credentialProvider; @Override @@ -35,10 +38,19 @@ public Member resolveArgument( AuthenticationPrinciple parameterAnnotation = parameter.getParameterAnnotation(AuthenticationPrinciple.class); boolean supported = Objects.nonNull(parameterAnnotation); HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - if (supported && !parameterAnnotation.required() && !credentialManager.hasCredential(request)) { + if (supported && !parameterAnnotation.required() && !hasCredential(request)) { return null; } + CredentialManager credentialManager = credentialManagers.stream() + .filter(eachCredentialManager -> eachCredentialManager.hasCredential(request)) + .findFirst() + .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인 해 주세요.")); Credential credential = credentialManager.getCredential(request); return credentialProvider.extractMember(credential); } + + private boolean hasCredential(HttpServletRequest request) { + return credentialManagers.stream() + .anyMatch(credentialManager -> credentialManager.hasCredential(request)); + } } diff --git a/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java b/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java index fd76b745e..bc7acadad 100644 --- a/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java +++ b/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java @@ -14,11 +14,11 @@ @RequiredArgsConstructor public class AuthWebConfiguration implements WebMvcConfigurer { - private final CredentialManager credentialManager; + private final List credentialManagers; private final CredentialProvider credentialProvider; @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(new AuthArgumentResolver(credentialManager, credentialProvider)); + resolvers.add(new AuthArgumentResolver(credentialManagers, credentialProvider)); } } diff --git a/backend/src/main/java/codezap/auth/controller/AuthController.java b/backend/src/main/java/codezap/auth/controller/AuthController.java index 22c85ba4b..068db186e 100644 --- a/backend/src/main/java/codezap/auth/controller/AuthController.java +++ b/backend/src/main/java/codezap/auth/controller/AuthController.java @@ -1,15 +1,18 @@ package codezap.auth.controller; +import codezap.auth.dto.Credential; import codezap.auth.dto.LoginMember; import codezap.auth.dto.request.LoginRequest; import codezap.auth.dto.response.LoginResponse; -import codezap.auth.dto.Credential; import codezap.auth.manager.CredentialManager; import codezap.auth.provider.CredentialProvider; import codezap.auth.service.AuthService; +import codezap.global.exception.CodeZapException; +import codezap.global.exception.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -21,7 +24,7 @@ @RequiredArgsConstructor public class AuthController implements SpringDocAuthController { - private final CredentialManager credentialManager; + private final List credentialManagers; private final CredentialProvider credentialProvider; private final AuthService authService; @@ -32,12 +35,19 @@ public ResponseEntity login( ) { LoginMember loginMember = authService.login(loginRequest); Credential credential = credentialProvider.createCredential(loginMember); - credentialManager.setCredential(httpServletResponse, credential); + credentialManagers.forEach( + credentialManager -> credentialManager.setCredential(httpServletResponse, credential) + ); return ResponseEntity.ok(LoginResponse.from(loginMember)); } @GetMapping("/login/check") public ResponseEntity checkLogin(HttpServletRequest httpServletRequest) { + //ArgumentResolver 와 동작이 일치 + CredentialManager credentialManager = credentialManagers.stream() + .filter(eachCredentialManager -> eachCredentialManager.hasCredential(httpServletRequest)) + .findFirst() + .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인 해 주세요.")); Credential credential = credentialManager.getCredential(httpServletRequest); credentialProvider.extractMember(credential); return ResponseEntity.ok().build(); @@ -45,7 +55,9 @@ public ResponseEntity checkLogin(HttpServletRequest httpServletRequest) { @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse httpServletResponse) { - credentialManager.removeCredential(httpServletResponse); + credentialManagers.forEach( + credentialManager -> credentialManager.removeCredential(httpServletResponse) + ); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java b/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java index 8e0a73799..57634c31e 100644 --- a/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java +++ b/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java @@ -7,7 +7,9 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +@Component @RequiredArgsConstructor public class AuthorizationHeaderCredentialManager implements CredentialManager { diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java new file mode 100644 index 000000000..06f96ffd2 --- /dev/null +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -0,0 +1,76 @@ +package codezap.auth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +import codezap.auth.dto.request.LoginRequest; +import codezap.global.MvcTest; +import codezap.member.dto.request.SignupRequest; +import jakarta.servlet.http.Cookie; +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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MvcResult; + +class AuthTest extends MvcTest { + + @Nested + @DisplayName("로그인 테스트") + class Login { + + @Nested + @DisplayName("로그인에 성공하면") + class Success { + + private final String name = "name"; + private final String password = "password123!"; + + private MvcResult loginResult; + + @BeforeEach + void successLogin() throws Exception { + signup(); + + var loginRequest = new LoginRequest(name, password); + loginResult = mvc.perform(post("/login") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andReturn(); + } + + private void signup() throws Exception { + SignupRequest signupRequest = new SignupRequest(name, password); + + mvc.perform(post("/signup") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupRequest))); + } + + @Test + @DisplayName("인증 정보에 대한 cookie 값이 들어간다.") + void responseCookie() { + //when & then + Cookie cookie = loginResult.getResponse().getCookie("credential"); + assertThat(cookie).isNotNull(); + } + + @Test + @DisplayName("인증 정보에 대한 Authorization 헤더 값이 들어간다.") + void responseHeader() { + //when & then + MockHttpServletResponse response = loginResult.getResponse(); + String authorizationHeader = response.getHeader(HttpHeaders.AUTHORIZATION); + assertThat(authorizationHeader).contains("Basic "); + } + + } + + + } +} diff --git a/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java b/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java index d5157996d..e2f6de3a8 100644 --- a/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java +++ b/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java @@ -5,8 +5,10 @@ import codezap.auth.dto.LoginMember; import codezap.auth.dto.Credential; +import codezap.auth.manager.AuthorizationHeaderCredentialManager; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -28,8 +30,10 @@ class AuthArgumentResolverTest { private final CredentialProvider credentialProvider = new PlainCredentialProvider(); - private final CredentialManager credentialManager = new CookieCredentialManager(); - private final AuthArgumentResolver authArgumentResolver = new AuthArgumentResolver(credentialManager, credentialProvider); + private final List credentialManagers = + List.of(new CookieCredentialManager(), new AuthorizationHeaderCredentialManager()); + + private final AuthArgumentResolver authArgumentResolver = new AuthArgumentResolver(credentialManagers, credentialProvider); @Nested @DisplayName("지원하는 파라미터 테스트") @@ -131,7 +135,7 @@ void noCredentialTest() { //when & then assertThatThrownBy(() -> resolveArgument(requiredMethod, nativeWebRequest)) .isInstanceOf(CodeZapException.class) - .hasMessage("쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."); + .hasMessage("인증 정보가 없습니다. 다시 로그인 해 주세요."); } @Test @@ -159,7 +163,8 @@ private Member resolveArgument(Method method, NativeWebRequest webRequest) { private void setCredentialCookie(MockHttpServletRequest request, Member member) { MockHttpServletResponse mockResponse = new MockHttpServletResponse(); Credential credential = credentialProvider.createCredential(LoginMember.from(member)); - credentialManager.setCredential(mockResponse, credential); + credentialManagers.forEach( + credentialManager -> credentialManager.setCredential(mockResponse, credential)); request.setCookies(mockResponse.getCookies()); } } diff --git a/backend/src/test/java/codezap/global/MvcTest.java b/backend/src/test/java/codezap/global/MvcTest.java new file mode 100644 index 000000000..bf41c8068 --- /dev/null +++ b/backend/src/test/java/codezap/global/MvcTest.java @@ -0,0 +1,26 @@ +package codezap.global; + +import codezap.global.cors.CorsProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest +@EnableConfigurationProperties(CorsProperties.class) +public abstract class MvcTest { + + protected MockMvc mvc; + protected ObjectMapper objectMapper; + + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext) { + mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .build(); + objectMapper = new ObjectMapper(); + } +} From 17668054e45c8ebf270e7d80105307aa4da2924d Mon Sep 17 00:00:00 2001 From: zangsu Date: Wed, 18 Dec 2024 16:38:02 +0900 Subject: [PATCH 02/16] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20=EC=9E=AC=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/codezap/auth/AuthTest.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index 06f96ffd2..a36b3f091 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -33,23 +33,8 @@ class Success { @BeforeEach void successLogin() throws Exception { - signup(); - - var loginRequest = new LoginRequest(name, password); - loginResult = mvc.perform(post("/login") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequest))) - .andReturn(); - } - - private void signup() throws Exception { - SignupRequest signupRequest = new SignupRequest(name, password); - - mvc.perform(post("/signup") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(signupRequest))); + signup(name, password); + loginResult = requestLogin(name, password); } @Test @@ -71,6 +56,21 @@ void responseHeader() { } + private MvcResult requestLogin(String name, String password) throws Exception { + return mvc.perform(post("/login") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new LoginRequest(name, password)))) + .andReturn(); + } + + private void signup(String name, String password) throws Exception { + SignupRequest signupRequest = new SignupRequest(name, password); + mvc.perform(post("/signup") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupRequest))); + } } } From b61fdf20e0c92467c502d8e04a599e6a3cde3b6e Mon Sep 17 00:00:00 2001 From: zangsu Date: Wed, 18 Dec 2024 17:08:58 +0900 Subject: [PATCH 03/16] =?UTF-8?q?test:=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B4=80=EC=A0=90=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/codezap/auth/AuthTest.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index a36b3f091..95d25ca51 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -1,7 +1,8 @@ package codezap.auth; -import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import codezap.auth.dto.request.LoginRequest; import codezap.global.MvcTest; @@ -23,7 +24,7 @@ class AuthTest extends MvcTest { class Login { @Nested - @DisplayName("로그인에 성공하면") + @DisplayName("로그인에 성공:") class Success { private final String name = "name"; @@ -38,20 +39,27 @@ void successLogin() throws Exception { } @Test - @DisplayName("인증 정보에 대한 cookie 값이 들어간다.") - void responseCookie() { - //when & then - Cookie cookie = loginResult.getResponse().getCookie("credential"); - assertThat(cookie).isNotNull(); + @DisplayName("정상적인 인증 쿠키 반환") + void responseCookie() throws Exception { + //when + Cookie[] cookies = loginResult.getResponse().getCookies(); + + //then + mvc.perform(get("/login/check") + .cookie(cookies)) + .andExpect(status().isOk()); } @Test - @DisplayName("인증 정보에 대한 Authorization 헤더 값이 들어간다.") - void responseHeader() { + @DisplayName("정상적인 인증 헤더 반환") + void responseHeader() throws Exception { //when & then MockHttpServletResponse response = loginResult.getResponse(); String authorizationHeader = response.getHeader(HttpHeaders.AUTHORIZATION); - assertThat(authorizationHeader).contains("Basic "); + + mvc.perform(get("/login/check") + .header(HttpHeaders.AUTHORIZATION, authorizationHeader)) + .andExpect(status().isOk()); } } From a24bdcc8980895707f036b07c15fe99ca60b5990 Mon Sep 17 00:00:00 2001 From: zangsu Date: Thu, 19 Dec 2024 15:26:09 +0900 Subject: [PATCH 04/16] =?UTF-8?q?test:=20Auth=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/codezap/auth/AuthTest.java | 97 ++++++++++++++++--- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index 95d25ca51..d74750d89 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -1,14 +1,19 @@ package codezap.auth; +import static org.hamcrest.text.MatchesPattern.matchesPattern; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import codezap.auth.dto.request.LoginRequest; import codezap.global.MvcTest; import codezap.member.dto.request.SignupRequest; import jakarta.servlet.http.Cookie; +import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -16,6 +21,7 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; class AuthTest extends MvcTest { @@ -35,7 +41,7 @@ class Success { @BeforeEach void successLogin() throws Exception { signup(name, password); - loginResult = requestLogin(name, password); + loginResult = requestLogin(name, password).andReturn(); } @Test @@ -61,24 +67,89 @@ void responseHeader() throws Exception { .header(HttpHeaders.AUTHORIZATION, authorizationHeader)) .andExpect(status().isOk()); } + } + + @Nested + @DisplayName("로그인 실패:") + class FailLogin { + + @Test + @DisplayName("회원가입 하지 않은 정보로 로그인 시도") + void noSignup() throws Exception { + String notExistName = "noSignup"; + requestLogin(notExistName, "password123!") + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.detail").value("존재하지 않는 아이디 %s 입니다.".formatted(notExistName))); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인 시도") + void wrongPassword() throws Exception { + String name = "name"; + String password = "password123!"; + signup(name, password); + + requestLogin(name, password) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.detail").value("로그인에 실패하였습니다. 비밀번호를 확인해주세요.")); + } + } + } + @Nested + @DisplayName("로그인 확인") + class CheckLogin { + + @Test + @DisplayName("실패: 인증 정보 없음") + void noCredential() throws Exception { + mvc.perform(get("/login/check")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.detail").value("인증 정보가 없습니다. 다시 로그인 해 주세요.")); } + } - private MvcResult requestLogin(String name, String password) throws Exception { - return mvc.perform(post("/login") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new LoginRequest(name, password)))) + @Nested + @DisplayName("로그아웃 성공:") + class Logout { + + @Test + @DisplayName("쿠키 인증 정보를 정상적으로 삭제") + void logoutWithCookie() throws Exception { + //given + String name = "name"; + String password = "password123!"; + signup(name, password); + MvcResult loginResponse = requestLogin(name, password).andReturn(); + Pattern expireCookieRegex = Pattern.compile("credential=.*?; Max-Age=0;.*?"); + + //when + mvc.perform(post("/logout") + .cookie(loginResponse.getResponse().getCookies())) + .andExpect(header().string(HttpHeaders.SET_COOKIE, matchesPattern(expireCookieRegex))) .andReturn(); } - private void signup(String name, String password) throws Exception { - SignupRequest signupRequest = new SignupRequest(name, password); - - mvc.perform(post("/signup") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(signupRequest))); + @Disabled + @Test + @DisplayName("Authorization 헤더의 로그아웃은 클라이언트에서 구현을 한다.") + void loginWithAuthorizationHeader() { } } + + private void signup(String name, String password) throws Exception { + SignupRequest signupRequest = new SignupRequest(name, password); + + mvc.perform(post("/signup") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupRequest))); + } + + private ResultActions requestLogin(String name, String password) throws Exception { + return mvc.perform(post("/login") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new LoginRequest(name, password)))); + } } From 6e8bd43117af2d4fb78a3896babf76c6adec3998 Mon Sep 17 00:00:00 2001 From: zangsu Date: Thu, 19 Dec 2024 20:29:06 +0900 Subject: [PATCH 05/16] =?UTF-8?q?test:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=97=90=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/codezap/auth/AuthTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index d74750d89..77375681d 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -89,7 +89,7 @@ void wrongPassword() throws Exception { String password = "password123!"; signup(name, password); - requestLogin(name, password) + requestLogin(name, "wrongPassword123!") .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.detail").value("로그인에 실패하였습니다. 비밀번호를 확인해주세요.")); } From 8569298c850e14f7b7ca308e92c2253ca6183cd5 Mon Sep 17 00:00:00 2001 From: zangsu Date: Sat, 21 Dec 2024 22:20:07 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=EB=A7=9E=EC=B6=A4=EB=B2=95?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EB=8F=84=EB=A1=9D=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codezap/auth/configuration/AuthArgumentResolver.java | 2 +- .../src/main/java/codezap/auth/controller/AuthController.java | 2 +- backend/src/test/java/codezap/auth/AuthTest.java | 2 +- .../codezap/auth/configuration/AuthArgumentResolverTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java index 79b10a38b..143e18814 100644 --- a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java +++ b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java @@ -44,7 +44,7 @@ public Member resolveArgument( CredentialManager credentialManager = credentialManagers.stream() .filter(eachCredentialManager -> eachCredentialManager.hasCredential(request)) .findFirst() - .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인 해 주세요.")); + .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요.")); Credential credential = credentialManager.getCredential(request); return credentialProvider.extractMember(credential); } diff --git a/backend/src/main/java/codezap/auth/controller/AuthController.java b/backend/src/main/java/codezap/auth/controller/AuthController.java index 068db186e..09f998614 100644 --- a/backend/src/main/java/codezap/auth/controller/AuthController.java +++ b/backend/src/main/java/codezap/auth/controller/AuthController.java @@ -47,7 +47,7 @@ public ResponseEntity checkLogin(HttpServletRequest httpServletRequest) { CredentialManager credentialManager = credentialManagers.stream() .filter(eachCredentialManager -> eachCredentialManager.hasCredential(httpServletRequest)) .findFirst() - .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인 해 주세요.")); + .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요.")); Credential credential = credentialManager.getCredential(httpServletRequest); credentialProvider.extractMember(credential); return ResponseEntity.ok().build(); diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index 77375681d..aea76a0d6 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -105,7 +105,7 @@ class CheckLogin { void noCredential() throws Exception { mvc.perform(get("/login/check")) .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.detail").value("인증 정보가 없습니다. 다시 로그인 해 주세요.")); + .andExpect(jsonPath("$.detail").value("인증 정보가 없습니다. 다시 로그인해 주세요.")); } } diff --git a/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java b/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java index e2f6de3a8..ee0ecf2a5 100644 --- a/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java +++ b/backend/src/test/java/codezap/auth/configuration/AuthArgumentResolverTest.java @@ -135,7 +135,7 @@ void noCredentialTest() { //when & then assertThatThrownBy(() -> resolveArgument(requiredMethod, nativeWebRequest)) .isInstanceOf(CodeZapException.class) - .hasMessage("인증 정보가 없습니다. 다시 로그인 해 주세요."); + .hasMessage("인증 정보가 없습니다. 다시 로그인해 주세요."); } @Test From d77eee09542dc8f0ce4f7d7ce04ea3dddce4d29e Mon Sep 17 00:00:00 2001 From: zangsu Date: Sat, 21 Dec 2024 22:22:06 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20zeusStyle=20=EB=A1=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/AuthArgumentResolver.java | 19 ++++++++++------- .../configuration/AuthWebConfiguration.java | 2 +- .../auth/controller/AuthController.java | 21 +++++++++++-------- .../java/codezap/auth/dto/LoginMember.java | 4 ++-- .../AuthorizationHeaderCredentialManager.java | 10 +++++---- .../auth/manager/CookieCredentialManager.java | 4 ++-- .../auth/manager/CredentialManager.java | 3 ++- .../auth/provider/CredentialProvider.java | 2 +- .../basic/BasicAuthCredentialProvider.java | 7 ++++--- .../codezap/auth/service/AuthService.java | 5 +++-- 10 files changed, 44 insertions(+), 33 deletions(-) diff --git a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java index 143e18814..c58e4f799 100644 --- a/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java +++ b/backend/src/main/java/codezap/auth/configuration/AuthArgumentResolver.java @@ -1,15 +1,10 @@ package codezap.auth.configuration; -import codezap.auth.dto.Credential; -import codezap.auth.manager.CredentialManager; -import codezap.auth.provider.CredentialProvider; -import codezap.global.exception.CodeZapException; -import codezap.global.exception.ErrorCode; -import codezap.member.domain.Member; -import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.Objects; -import lombok.RequiredArgsConstructor; + +import jakarta.servlet.http.HttpServletRequest; + import org.springframework.core.MethodParameter; import org.springframework.lang.NonNull; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -17,6 +12,14 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import codezap.auth.dto.Credential; +import codezap.auth.manager.CredentialManager; +import codezap.auth.provider.CredentialProvider; +import codezap.global.exception.CodeZapException; +import codezap.global.exception.ErrorCode; +import codezap.member.domain.Member; +import lombok.RequiredArgsConstructor; + @RequiredArgsConstructor public class AuthArgumentResolver implements HandlerMethodArgumentResolver { diff --git a/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java b/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java index bc7acadad..4cd8bdc49 100644 --- a/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java +++ b/backend/src/main/java/codezap/auth/configuration/AuthWebConfiguration.java @@ -1,6 +1,5 @@ package codezap.auth.configuration; -import codezap.auth.provider.CredentialProvider; import java.util.List; import org.springframework.context.annotation.Configuration; @@ -8,6 +7,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import codezap.auth.manager.CredentialManager; +import codezap.auth.provider.CredentialProvider; import lombok.RequiredArgsConstructor; @Configuration diff --git a/backend/src/main/java/codezap/auth/controller/AuthController.java b/backend/src/main/java/codezap/auth/controller/AuthController.java index 09f998614..20a4d25dd 100644 --- a/backend/src/main/java/codezap/auth/controller/AuthController.java +++ b/backend/src/main/java/codezap/auth/controller/AuthController.java @@ -1,5 +1,17 @@ package codezap.auth.controller; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + import codezap.auth.dto.Credential; import codezap.auth.dto.LoginMember; import codezap.auth.dto.request.LoginRequest; @@ -9,16 +21,7 @@ import codezap.auth.service.AuthService; import codezap.global.exception.CodeZapException; import codezap.global.exception.ErrorCode; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor diff --git a/backend/src/main/java/codezap/auth/dto/LoginMember.java b/backend/src/main/java/codezap/auth/dto/LoginMember.java index 30bdd87fb..ac3809cd5 100644 --- a/backend/src/main/java/codezap/auth/dto/LoginMember.java +++ b/backend/src/main/java/codezap/auth/dto/LoginMember.java @@ -2,12 +2,12 @@ import codezap.member.domain.Member; -public record LoginMember ( +public record LoginMember( long id, String name, String password, String salt -){ +) { public static LoginMember from(Member member) { return new LoginMember(member.getId(), member.getName(), member.getPassword(), member.getSalt()); } diff --git a/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java b/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java index 57634c31e..c7f195559 100644 --- a/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java +++ b/backend/src/main/java/codezap/auth/manager/AuthorizationHeaderCredentialManager.java @@ -1,14 +1,16 @@ package codezap.auth.manager; -import codezap.auth.dto.Credential; -import codezap.global.exception.CodeZapException; -import codezap.global.exception.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; + import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; +import codezap.auth.dto.Credential; +import codezap.global.exception.CodeZapException; +import codezap.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; + @Component @RequiredArgsConstructor public class AuthorizationHeaderCredentialManager implements CredentialManager { diff --git a/backend/src/main/java/codezap/auth/manager/CookieCredentialManager.java b/backend/src/main/java/codezap/auth/manager/CookieCredentialManager.java index b01c2a712..e57fcfbf7 100644 --- a/backend/src/main/java/codezap/auth/manager/CookieCredentialManager.java +++ b/backend/src/main/java/codezap/auth/manager/CookieCredentialManager.java @@ -1,20 +1,20 @@ package codezap.auth.manager; -import codezap.auth.dto.Credential; import java.util.Arrays; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; +import codezap.auth.dto.Credential; import codezap.global.exception.CodeZapException; import codezap.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor diff --git a/backend/src/main/java/codezap/auth/manager/CredentialManager.java b/backend/src/main/java/codezap/auth/manager/CredentialManager.java index 5bd78998a..b0b942b63 100644 --- a/backend/src/main/java/codezap/auth/manager/CredentialManager.java +++ b/backend/src/main/java/codezap/auth/manager/CredentialManager.java @@ -1,9 +1,10 @@ package codezap.auth.manager; -import codezap.auth.dto.Credential; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import codezap.auth.dto.Credential; + /** * Credential 정보를 Http 응답에 설정하기 위한 클래스입니다. */ diff --git a/backend/src/main/java/codezap/auth/provider/CredentialProvider.java b/backend/src/main/java/codezap/auth/provider/CredentialProvider.java index 0e7b6c8d7..06c705a10 100644 --- a/backend/src/main/java/codezap/auth/provider/CredentialProvider.java +++ b/backend/src/main/java/codezap/auth/provider/CredentialProvider.java @@ -1,7 +1,7 @@ package codezap.auth.provider; -import codezap.auth.dto.LoginMember; import codezap.auth.dto.Credential; +import codezap.auth.dto.LoginMember; import codezap.member.domain.Member; /** diff --git a/backend/src/main/java/codezap/auth/provider/basic/BasicAuthCredentialProvider.java b/backend/src/main/java/codezap/auth/provider/basic/BasicAuthCredentialProvider.java index 9fc5c2e8d..752e84543 100644 --- a/backend/src/main/java/codezap/auth/provider/basic/BasicAuthCredentialProvider.java +++ b/backend/src/main/java/codezap/auth/provider/basic/BasicAuthCredentialProvider.java @@ -1,7 +1,5 @@ package codezap.auth.provider.basic; -import codezap.auth.dto.LoginMember; -import codezap.auth.dto.Credential; import java.nio.charset.StandardCharsets; import jakarta.servlet.http.HttpServletRequest; @@ -9,6 +7,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; +import codezap.auth.dto.Credential; +import codezap.auth.dto.LoginMember; import codezap.auth.provider.CredentialProvider; import codezap.global.exception.CodeZapException; import codezap.global.exception.ErrorCode; @@ -24,7 +24,8 @@ public class BasicAuthCredentialProvider implements CredentialProvider { @Override public Credential createCredential(LoginMember loginMember) { - String credentialValue = HttpHeaders.encodeBasicAuth(loginMember.name(), loginMember.password(), StandardCharsets.UTF_8); + String credentialValue = HttpHeaders.encodeBasicAuth(loginMember.name(), loginMember.password(), + StandardCharsets.UTF_8); return Credential.basic(credentialValue); } diff --git a/backend/src/main/java/codezap/auth/service/AuthService.java b/backend/src/main/java/codezap/auth/service/AuthService.java index e08ff3a81..b83545ef8 100644 --- a/backend/src/main/java/codezap/auth/service/AuthService.java +++ b/backend/src/main/java/codezap/auth/service/AuthService.java @@ -1,5 +1,8 @@ package codezap.auth.service; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import codezap.auth.dto.LoginMember; import codezap.auth.dto.request.LoginRequest; import codezap.auth.encryption.PasswordEncryptor; @@ -8,8 +11,6 @@ import codezap.member.domain.Member; import codezap.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor From 320e655aabd2b3ec220f43ef898d8e8fb89945ff Mon Sep 17 00:00:00 2001 From: zangsu Date: Sat, 21 Dec 2024 22:23:34 +0900 Subject: [PATCH 08/16] =?UTF-8?q?test:=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=EC=9D=98=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/codezap/auth/AuthTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthTest.java index aea76a0d6..4fb9e88c3 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthTest.java @@ -30,7 +30,7 @@ class AuthTest extends MvcTest { class Login { @Nested - @DisplayName("로그인에 성공:") + @DisplayName("성공: 올바른 아이디와 비밀번호를 사용하여 로그인에 성공") class Success { private final String name = "name"; From b968ca2e4ad769150e8d89a18b2c3b243e681c45 Mon Sep 17 00:00:00 2001 From: zangsu Date: Sat, 21 Dec 2024 23:09:40 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codezap/auth/controller/AuthController.java | 17 +++++++++-------- .../controller/SpringDocAuthController.java | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/codezap/auth/controller/AuthController.java b/backend/src/main/java/codezap/auth/controller/AuthController.java index 20a4d25dd..c6a3049c7 100644 --- a/backend/src/main/java/codezap/auth/controller/AuthController.java +++ b/backend/src/main/java/codezap/auth/controller/AuthController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import codezap.auth.configuration.AuthenticationPrinciple; import codezap.auth.dto.Credential; import codezap.auth.dto.LoginMember; import codezap.auth.dto.request.LoginRequest; @@ -21,6 +22,7 @@ import codezap.auth.service.AuthService; import codezap.global.exception.CodeZapException; import codezap.global.exception.ErrorCode; +import codezap.member.domain.Member; import lombok.RequiredArgsConstructor; @RestController @@ -45,14 +47,13 @@ public ResponseEntity login( } @GetMapping("/login/check") - public ResponseEntity checkLogin(HttpServletRequest httpServletRequest) { - //ArgumentResolver 와 동작이 일치 - CredentialManager credentialManager = credentialManagers.stream() - .filter(eachCredentialManager -> eachCredentialManager.hasCredential(httpServletRequest)) - .findFirst() - .orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요.")); - Credential credential = credentialManager.getCredential(httpServletRequest); - credentialProvider.extractMember(credential); + public ResponseEntity checkLogin( + @AuthenticationPrinciple Member member, + HttpServletRequest httpServletRequest + ) { + if (member == null) { + throw new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요."); + } return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/codezap/auth/controller/SpringDocAuthController.java b/backend/src/main/java/codezap/auth/controller/SpringDocAuthController.java index 9bfe6c96b..bc42df75f 100644 --- a/backend/src/main/java/codezap/auth/controller/SpringDocAuthController.java +++ b/backend/src/main/java/codezap/auth/controller/SpringDocAuthController.java @@ -10,6 +10,7 @@ import codezap.auth.dto.response.LoginResponse; import codezap.global.swagger.error.ApiErrorResponse; import codezap.global.swagger.error.ErrorCase; +import codezap.member.domain.Member; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -41,7 +42,7 @@ public interface SpringDocAuthController { @ErrorCase(description = "쿠키 없음", exampleMessage = "쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."), @ErrorCase(description = "인증 쿠키 없음", exampleMessage = "인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."), }) - ResponseEntity checkLogin(HttpServletRequest request); + ResponseEntity checkLogin(Member member, HttpServletRequest request); @Operation(summary = "로그아웃") @ApiResponse(responseCode = "204", description = "인증 성공") From f27c763b259692bfc19a3b6c85902e69cad73f22 Mon Sep 17 00:00:00 2001 From: zangsu Date: Sat, 21 Dec 2024 23:10:51 +0900 Subject: [PATCH 10/16] =?UTF-8?q?test:=20=EC=9D=B8=EC=88=98=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codezap/auth/{AuthTest.java => AuthAcceptanceTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename backend/src/test/java/codezap/auth/{AuthTest.java => AuthAcceptanceTest.java} (99%) diff --git a/backend/src/test/java/codezap/auth/AuthTest.java b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java similarity index 99% rename from backend/src/test/java/codezap/auth/AuthTest.java rename to backend/src/test/java/codezap/auth/AuthAcceptanceTest.java index 4fb9e88c3..3452fe02b 100644 --- a/backend/src/test/java/codezap/auth/AuthTest.java +++ b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java @@ -23,7 +23,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -class AuthTest extends MvcTest { +class AuthAcceptanceTest extends MvcTest { @Nested @DisplayName("로그인 테스트") From 0a46b270f3378d817ca801309812646793ceeec8 Mon Sep 17 00:00:00 2001 From: zangsu Date: Mon, 23 Dec 2024 22:19:04 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20MockTest=20=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20ObjectMapper=20?= =?UTF-8?q?=EB=A5=BC=20@Autowired=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/codezap/global/MvcTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/test/java/codezap/global/MvcTest.java b/backend/src/test/java/codezap/global/MvcTest.java index bf41c8068..7d9298222 100644 --- a/backend/src/test/java/codezap/global/MvcTest.java +++ b/backend/src/test/java/codezap/global/MvcTest.java @@ -3,6 +3,7 @@ import codezap.global.cors.CorsProperties; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; @@ -14,13 +15,13 @@ public abstract class MvcTest { protected MockMvc mvc; - protected ObjectMapper objectMapper; + @Autowired + protected ObjectMapper objectMapper; @BeforeEach void setUp(WebApplicationContext webApplicationContext) { mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .build(); - objectMapper = new ObjectMapper(); } } From fae61a73d8a1ae6e76e12ba8c7ea62c621a56493 Mon Sep 17 00:00:00 2001 From: zangsu Date: Mon, 23 Dec 2024 22:20:10 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor:=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=9D=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EB=8D=94=20=EB=AA=85=ED=99=95=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/codezap/auth/AuthAcceptanceTest.java | 4 ++-- .../codezap/global/{MvcTest.java => IntegrationTest.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename backend/src/test/java/codezap/global/{MvcTest.java => IntegrationTest.java} (96%) diff --git a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java index 3452fe02b..4c09bdafb 100644 --- a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java @@ -8,7 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import codezap.auth.dto.request.LoginRequest; -import codezap.global.MvcTest; +import codezap.global.IntegrationTest; import codezap.member.dto.request.SignupRequest; import jakarta.servlet.http.Cookie; import java.util.regex.Pattern; @@ -23,7 +23,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -class AuthAcceptanceTest extends MvcTest { +class AuthAcceptanceTest extends IntegrationTest { @Nested @DisplayName("로그인 테스트") diff --git a/backend/src/test/java/codezap/global/MvcTest.java b/backend/src/test/java/codezap/global/IntegrationTest.java similarity index 96% rename from backend/src/test/java/codezap/global/MvcTest.java rename to backend/src/test/java/codezap/global/IntegrationTest.java index 7d9298222..fb12ef5d5 100644 --- a/backend/src/test/java/codezap/global/MvcTest.java +++ b/backend/src/test/java/codezap/global/IntegrationTest.java @@ -12,7 +12,7 @@ @SpringBootTest @EnableConfigurationProperties(CorsProperties.class) -public abstract class MvcTest { +public class IntegrationTest { protected MockMvc mvc; From a278e1646eff36c028b2a931dd8458ea3b51a355 Mon Sep 17 00:00:00 2001 From: zangsu Date: Mon, 23 Dec 2024 22:23:30 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8F=AC=ED=8A=B8=EB=A5=BC=20RANDOM=5FPORT=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/codezap/global/IntegrationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/codezap/global/IntegrationTest.java b/backend/src/test/java/codezap/global/IntegrationTest.java index fb12ef5d5..863d26a3d 100644 --- a/backend/src/test/java/codezap/global/IntegrationTest.java +++ b/backend/src/test/java/codezap/global/IntegrationTest.java @@ -6,11 +6,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @EnableConfigurationProperties(CorsProperties.class) public class IntegrationTest { From 873c9d1b3fa4b7acc344d98896fd466fbb581b76 Mon Sep 17 00:00:00 2001 From: zangsu Date: Fri, 27 Dec 2024 17:34:00 +0900 Subject: [PATCH 14/16] =?UTF-8?q?test:=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codezap/auth/AuthAcceptanceTest.java | 47 ++++--- .../java/codezap/global/IntegrationTest.java | 121 +++++++++++++++++- 2 files changed, 145 insertions(+), 23 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java index 4c09bdafb..8a52ac05a 100644 --- a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java @@ -1,28 +1,29 @@ package codezap.auth; -import static org.hamcrest.text.MatchesPattern.matchesPattern; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import codezap.auth.dto.request.LoginRequest; -import codezap.global.IntegrationTest; -import codezap.member.dto.request.SignupRequest; import jakarta.servlet.http.Cookie; -import java.util.regex.Pattern; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import codezap.auth.dto.request.LoginRequest; +import codezap.category.dto.request.CreateCategoryRequest; +import codezap.category.dto.response.CreateCategoryResponse; +import codezap.global.IntegrationTest; +import codezap.member.dto.request.SignupRequest; + class AuthAcceptanceTest extends IntegrationTest { @Nested @@ -110,24 +111,32 @@ void noCredential() throws Exception { } @Nested - @DisplayName("로그아웃 성공:") + @DisplayName("로그아웃") class Logout { @Test - @DisplayName("쿠키 인증 정보를 정상적으로 삭제") + @DisplayName("성공") void logoutWithCookie() throws Exception { //given String name = "name"; String password = "password123!"; signup(name, password); - MvcResult loginResponse = requestLogin(name, password).andReturn(); - Pattern expireCookieRegex = Pattern.compile("credential=.*?; Max-Age=0;.*?"); + requestLogin(name, password); + + MockHttpServletResponse createCategoryResponse = request(post("/categories") + .content(objectMapper.writeValueAsString(new CreateCategoryRequest("new category")))) + .andReturn().getResponse(); + long createdCategoryId = objectMapper.readValue( + createCategoryResponse.getContentAsString(), + CreateCategoryResponse.class + ).id(); //when - mvc.perform(post("/logout") - .cookie(loginResponse.getResponse().getCookies())) - .andExpect(header().string(HttpHeaders.SET_COOKIE, matchesPattern(expireCookieRegex))) - .andReturn(); + request(post("/logout")); + + //then + request(delete("/categories/" + createdCategoryId)) + .andExpect(status().isNoContent()); } @Disabled @@ -140,16 +149,12 @@ void loginWithAuthorizationHeader() { private void signup(String name, String password) throws Exception { SignupRequest signupRequest = new SignupRequest(name, password); - mvc.perform(post("/signup") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) + request(post("/signup") .content(objectMapper.writeValueAsString(signupRequest))); } private ResultActions requestLogin(String name, String password) throws Exception { - return mvc.perform(post("/login") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) + return request(post("/login") .content(objectMapper.writeValueAsString(new LoginRequest(name, password)))); } } diff --git a/backend/src/test/java/codezap/global/IntegrationTest.java b/backend/src/test/java/codezap/global/IntegrationTest.java index 863d26a3d..2cf04e116 100644 --- a/backend/src/test/java/codezap/global/IntegrationTest.java +++ b/backend/src/test/java/codezap/global/IntegrationTest.java @@ -1,21 +1,72 @@ package codezap.global; -import codezap.global.cors.CorsProperties; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.Cookie; + import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import com.fasterxml.jackson.databind.ObjectMapper; + +import codezap.global.cors.CorsProperties; + @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @EnableConfigurationProperties(CorsProperties.class) public class IntegrationTest { protected MockMvc mvc; + protected Cookies cookies; + + private static class Cookies { + List cookies = new ArrayList<>(); + + private void addAll(Cookie... newCookies) { + Arrays.stream(newCookies) + .filter(cookies::contains) + .forEach(cookies::add); + } + + private Cookie[] getCookies() { + return cookies.toArray(Cookie[]::new); + } + + private void setCookie(CookieValue cookieValue) { + if (!containsNameOf(cookieValue.name)) { + cookies.add(cookieValue.toCookie()); + return; + } + + Cookie existsCookie = cookies.stream() + .filter(cookie -> cookie.getName().equals(cookieValue.name)) + .findFirst() + .get(); + cookieValue.apply(existsCookie); + } + + private boolean containsNameOf(String name) { + return cookies.stream().anyMatch(cookie -> cookie.getName().equals(name)); + } + + public boolean isEmpty() { + return cookies.isEmpty(); + } + } @Autowired protected ObjectMapper objectMapper; @@ -24,5 +75,71 @@ public class IntegrationTest { void setUp(WebApplicationContext webApplicationContext) { mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .build(); + cookies = new Cookies(); + } + + protected ResultActions request(MockHttpServletRequestBuilder requestBuilder) throws Exception { + requestBuilder = requestBuilder + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON); + if (!cookies.isEmpty()) { + requestBuilder = requestBuilder.cookie(cookies.getCookies()); + } + + ResultActions perform = mvc.perform(requestBuilder); + + MockHttpServletResponse response = perform.andReturn().getResponse(); + cookies.addAll(response.getCookies()); + + if (response.containsHeader(HttpHeaders.SET_COOKIE)) { + CookieValue cookieValue = new CookieValue(response.getHeader(HttpHeaders.SET_COOKIE)); + cookies.setCookie(cookieValue); + } + return perform; + } + + private static class CookieValue { + private static final List NO_VALUE_ATTRIBUTES = List.of("Secure", "HttpOnly"); + private final String name; + private final String value; + private final int maxAge; + + public CookieValue(String header) { + String[] split = header.split(";"); + String[] nameAndValue = split[0].split("=", 2); + + Map attributes = new HashMap<>(); + for (int i = 1; i < split.length; i++) { + String token = split[i].strip(); + if (NO_VALUE_ATTRIBUTES.contains(token)) { + attributes.put(token, Boolean.TRUE); + continue; + } + String[] attributesValue = token.split("="); + attributes.put(attributesValue[0], attributesValue[1]); + } + + this.name = nameAndValue[0]; + this.value = nameAndValue[1]; + this.maxAge = getExpires(attributes); + } + + private int getExpires(Map attributes) { + if (attributes.containsKey("Max-Age")) { + String maxAge = (String) attributes.get("Max-Age"); + return Integer.parseInt(maxAge); + } + return -1; + } + + public void apply(Cookie cookie) { + cookie.setMaxAge(maxAge); + } + + public Cookie toCookie() { + Cookie cookie = new Cookie(this.name, this.value); + cookie.setMaxAge(this.maxAge); + return cookie; + } } } From 9eb729a3f504ff7011ad1c06176e97d2e5bed924 Mon Sep 17 00:00:00 2001 From: zangsu Date: Fri, 27 Dec 2024 20:49:57 +0900 Subject: [PATCH 15/16] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codezap/auth/AuthAcceptanceTest.java | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java index 8a52ac05a..71957b9fe 100644 --- a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java @@ -6,16 +6,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import jakarta.servlet.http.Cookie; - -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import codezap.auth.dto.request.LoginRequest; @@ -24,50 +19,28 @@ import codezap.global.IntegrationTest; import codezap.member.dto.request.SignupRequest; + class AuthAcceptanceTest extends IntegrationTest { @Nested @DisplayName("로그인 테스트") class Login { - @Nested + @Test @DisplayName("성공: 올바른 아이디와 비밀번호를 사용하여 로그인에 성공") - class Success { - - private final String name = "name"; - private final String password = "password123!"; - - private MvcResult loginResult; - - @BeforeEach - void successLogin() throws Exception { - signup(name, password); - loginResult = requestLogin(name, password).andReturn(); - } + void success() throws Exception { + //given + String name = "name"; + String password = "password123!"; + signup(name, password); - @Test - @DisplayName("정상적인 인증 쿠키 반환") - void responseCookie() throws Exception { - //when - Cookie[] cookies = loginResult.getResponse().getCookies(); - - //then - mvc.perform(get("/login/check") - .cookie(cookies)) - .andExpect(status().isOk()); - } + //when + login(name, password); - @Test - @DisplayName("정상적인 인증 헤더 반환") - void responseHeader() throws Exception { - //when & then - MockHttpServletResponse response = loginResult.getResponse(); - String authorizationHeader = response.getHeader(HttpHeaders.AUTHORIZATION); - - mvc.perform(get("/login/check") - .header(HttpHeaders.AUTHORIZATION, authorizationHeader)) - .andExpect(status().isOk()); - } + //then + request(post("/categories") + .content(objectMapper.writeValueAsString(new CreateCategoryRequest("new category")))) + .andExpect(status().isCreated()); } @Nested @@ -78,7 +51,7 @@ class FailLogin { @DisplayName("회원가입 하지 않은 정보로 로그인 시도") void noSignup() throws Exception { String notExistName = "noSignup"; - requestLogin(notExistName, "password123!") + login(notExistName, "password123!") .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.detail").value("존재하지 않는 아이디 %s 입니다.".formatted(notExistName))); } @@ -90,7 +63,7 @@ void wrongPassword() throws Exception { String password = "password123!"; signup(name, password); - requestLogin(name, "wrongPassword123!") + login(name, "wrongPassword123!") .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.detail").value("로그인에 실패하였습니다. 비밀번호를 확인해주세요.")); } @@ -121,7 +94,7 @@ void logoutWithCookie() throws Exception { String name = "name"; String password = "password123!"; signup(name, password); - requestLogin(name, password); + login(name, password); MockHttpServletResponse createCategoryResponse = request(post("/categories") .content(objectMapper.writeValueAsString(new CreateCategoryRequest("new category")))) @@ -153,7 +126,7 @@ private void signup(String name, String password) throws Exception { .content(objectMapper.writeValueAsString(signupRequest))); } - private ResultActions requestLogin(String name, String password) throws Exception { + private ResultActions login(String name, String password) throws Exception { return request(post("/login") .content(objectMapper.writeValueAsString(new LoginRequest(name, password)))); } From 41f5e1a9261a9e2dfac8c1c2cbb487d93d69d613 Mon Sep 17 00:00:00 2001 From: zangsu Date: Fri, 27 Dec 2024 20:54:59 +0900 Subject: [PATCH 16/16] =?UTF-8?q?test:=20=EC=9D=B8=EC=88=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A0=84=EC=B2=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codezap/auth/AuthAcceptanceTest.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java index 71957b9fe..b2a9582f8 100644 --- a/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/codezap/auth/AuthAcceptanceTest.java @@ -22,6 +22,9 @@ class AuthAcceptanceTest extends IntegrationTest { + private final String name = "name"; + private final String password = "password123!"; + @Nested @DisplayName("로그인 테스트") class Login { @@ -30,8 +33,6 @@ class Login { @DisplayName("성공: 올바른 아이디와 비밀번호를 사용하여 로그인에 성공") void success() throws Exception { //given - String name = "name"; - String password = "password123!"; signup(name, password); //when @@ -74,10 +75,22 @@ void wrongPassword() throws Exception { @DisplayName("로그인 확인") class CheckLogin { + @Test + @DisplayName("성공: 인증정보가 있을 경우 200 OK 를 반환") + void success() throws Exception { + //given + signup(name, password); + login(name, password); + + //when + request(get("/login/check")) + .andExpect(status().isOk()); + } + @Test @DisplayName("실패: 인증 정보 없음") void noCredential() throws Exception { - mvc.perform(get("/login/check")) + request(get("/login/check")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.detail").value("인증 정보가 없습니다. 다시 로그인해 주세요.")); } @@ -88,7 +101,7 @@ void noCredential() throws Exception { class Logout { @Test - @DisplayName("성공") + @DisplayName("성공: 로그아웃 이후에는 카테고리를 수정할 수 없음") void logoutWithCookie() throws Exception { //given String name = "name";