ADD session maintaining

- @LoginAccount 파라메타를 command 객체로 바꾸는 ArgumentResolver 추가
- .editorconfig 에서 로그파일 (app.log)를 무시하도록 설정 추가
This commit is contained in:
JiwonDev
2021-09-11 03:17:00 +09:00
committed by Jiwon
parent 72c25e23ee
commit d3ff00e167
13 changed files with 264 additions and 5 deletions

View File

@@ -8,6 +8,15 @@ charset = utf-8
# [newline-eof]
insert_final_newline = true
# Ignore
[*.log]
charset = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
indent_style = unset
indent_size = unset
[*.bat]
end_of_line = crlf
@@ -23,4 +32,4 @@ tab_width = 4
trim_trailing_whitespace = true
# [line-length-100]
max_line_length = 100
max_line_length = 100

View File

@@ -90,6 +90,7 @@ jacocoTestCoverageVerification {
enabled = true
element = 'CLASS'
excludes = [
"*.*ArgumentResolver",
"*.YouAndMeApplication",
"*.*Request",
"*.*Response",

View File

@@ -1,11 +1,13 @@
package com.yam.app.account.application;
import com.yam.app.account.domain.AccountPrincipal;
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
import com.yam.app.account.domain.LoginAccountProcessor;
import com.yam.app.account.domain.RegisterAccountEvent;
import com.yam.app.account.domain.RegisterAccountProcessor;
import com.yam.app.account.presentation.AccountResponse;
import com.yam.app.account.presentation.ConfirmRegisterAccountRequestCommand;
import com.yam.app.account.presentation.GetSessionAccountCommand;
import com.yam.app.account.presentation.LoginAccountRequestCommand;
import com.yam.app.account.presentation.RegisterAccountRequestCommand;
import org.springframework.context.ApplicationEventPublisher;
@@ -20,16 +22,19 @@ public class AccountFacade {
private final ApplicationEventPublisher publisher;
private final ConfirmRegisterAccountProcessor confirmRegisterProcessor;
private final LoginAccountProcessor loginProcessor;
private final AccountPrincipal accountPrincipal;
public AccountFacade(RegisterAccountProcessor registerProcessor,
AccountTranslator translator, ApplicationEventPublisher publisher,
ConfirmRegisterAccountProcessor confirmRegisterProcessor,
LoginAccountProcessor loginProcessor) {
LoginAccountProcessor loginProcessor,
AccountPrincipal accountPrincipal) {
this.registerProcessor = registerProcessor;
this.translator = translator;
this.publisher = publisher;
this.confirmRegisterProcessor = confirmRegisterProcessor;
this.loginProcessor = loginProcessor;
this.accountPrincipal = accountPrincipal;
}
@Transactional
@@ -48,7 +53,13 @@ public class AccountFacade {
confirmRegisterProcessor.registerConfirm(request.getToken(), request.getEmail());
}
@Transactional(readOnly = true)
public void login(LoginAccountRequestCommand request) {
loginProcessor.login(request.getEmail(), request.getPassword());
}
@Transactional(readOnly = true)
public AccountResponse getSessionAccount(GetSessionAccountCommand command) {
return translator.toResponse(accountPrincipal.getAccount(command.getEmail()));
}
}

View File

@@ -0,0 +1,16 @@
package com.yam.app.account.domain;
public final class AccountPrincipal {
private final AccountReader accountReader;
public AccountPrincipal(AccountReader accountReader) {
this.accountReader = accountReader;
}
public Account getAccount(String email) {
return accountReader.findByEmail(email)
.orElseThrow(IllegalStateException::new);
}
}

View File

@@ -1,5 +1,6 @@
package com.yam.app.account.infrastructure;
import com.yam.app.account.domain.AccountPrincipal;
import com.yam.app.account.domain.AccountReader;
import com.yam.app.account.domain.AccountRepository;
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
@@ -69,4 +70,9 @@ public class AppConfiguration {
return new LoginAccountProcessor(accountReader, passwordEncrypter);
}
@Bean
public AccountPrincipal accountPrincipal(AccountReader accountReader) {
return new AccountPrincipal(accountReader);
}
}

View File

@@ -5,6 +5,8 @@ import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.RequestMapping;
@@ -35,4 +37,16 @@ public final class AccountQueryApi {
LoginSessionUtils.setLoginAccountEmail(request.getEmail());
return ResponseEntity.ok().build();
}
@GetMapping("/api/accounts/me")
public ResponseEntity<AccountResponse> getAccount(
@LoginAccount GetSessionAccountCommand request) {
return ResponseEntity.ok(accountFacade.getSessionAccount(request));
}
// LoginAccountMethodArgumentResolver 테스트를 위해 임시로 만든 @ExceptionHandler
@ExceptionHandler
public ResponseEntity<Void> defaultException(IllegalStateException e) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,14 @@
package com.yam.app.account.presentation;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public final class GetSessionAccountCommand {
@NotBlank
@Email
private String email;
}

View File

@@ -0,0 +1,12 @@
package com.yam.app.account.presentation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAccount {
}

View File

@@ -0,0 +1,36 @@
package com.yam.app.account.presentation;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public final class LoginAccountMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginAccount.class)
&& GetSessionAccountCommand.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpSession session = ((HttpServletRequest) webRequest.getNativeRequest())
.getSession(false);
if (session == null) {
throw new IllegalStateException();
}
var command = new GetSessionAccountCommand();
command.setEmail((String) session.getAttribute(LoginSessionUtils.LOGIN_ACCOUNT_EMAIL));
return command;
}
}

View File

@@ -0,0 +1,23 @@
package com.yam.app.account.presentation;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private final LoginAccountMethodArgumentResolver loginAccountMethodArgumentResolver;
public WebConfiguration(
LoginAccountMethodArgumentResolver loginAccountMethodArgumentResolver) {
this.loginAccountMethodArgumentResolver = loginAccountMethodArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginAccountMethodArgumentResolver);
}
}

View File

@@ -23,4 +23,7 @@ alter table account
insert into account(email, email_check_token, email_check_token_generated_at, email_verified,
joined_at, last_modified_at, nickname, password, withdraw, role)
values ('jiwonDev@gmail.com', 'emailchecktoken', now(), false, now(), now(), 'jiwon', 'password!',
false, 'DEFAULT'),
('loginCheck@gmail.com', 'emailchecktoken1', now(), true, now(), now(), 'loginCheck',
'$2a$10$EqbMbYB0vcZnuA5CClqa9uiLDnjA6pCjxn208ZchzA2q3ofqnkhcq',
false, 'DEFAULT');

View File

@@ -8,13 +8,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.account.presentation.LoginAccountRequestCommand;
import com.yam.app.account.presentation.LoginSessionUtils;
import com.yam.app.account.presentation.RegisterAccountRequestCommand;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
@@ -24,12 +29,23 @@ import org.springframework.test.web.servlet.MockMvc;
@ActiveProfiles("test")
final class AccountIntegrationTests {
private MockHttpSession session;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
public void setUp() {
session = new MockHttpSession();
session.setAttribute(LoginSessionUtils.LOGIN_ACCOUNT_EMAIL, "loginCheck@gmail.com");
}
@AfterEach
public void clean() {
session.clearAttributes();
}
@Test
@DisplayName("새로운 계정을 등록하는 회원가입 시나리오")
void new_account_request_in_register_correctly() throws Exception {
@@ -73,4 +89,44 @@ final class AccountIntegrationTests {
.andExpect(redirectedUrl("http://localhost:3000/login"));
}
@Test
@DisplayName("로그인 요청을 성공하여 서버의 세션 등록을 완료하는 시나리오")
void login_success_get_session() throws Exception {
//Arrange
LoginAccountRequestCommand request = new LoginAccountRequestCommand();
request.setEmail("loginCheck@gmail.com");
request.setPassword("password!");
//Act
final var actions = mockMvc.perform(post("/api/accounts/login")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
//Assert
actions
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("로그인한 회원이 세션에서 자신의 정보를 받아오는 시나리오")
void login_member_get_account_session_request() throws Exception {
//Act
final var actions = mockMvc.perform(get("/api/accounts/me")
.session(session)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON));
//Assert
actions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.email").isString())
.andExpect(jsonPath("$.nickname").isString());
}
}

View File

@@ -2,12 +2,17 @@ package com.yam.app.account.presentation;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
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.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.account.application.AccountFacade;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -18,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
@DisplayName("Account Query HTTP API")
@@ -35,6 +41,58 @@ class AccountQueryApiTest {
@DisplayName("Login HTTP API")
class LoginApi {
private final String loggedInEmail = "loginCheck@gmail.com";
private MockHttpSession session;
@BeforeEach
public void setUp() {
session = new MockHttpSession();
session.setAttribute(LoginSessionUtils.LOGIN_ACCOUNT_EMAIL, loggedInEmail);
}
@AfterEach
public void clean() {
session.clearAttributes();
}
@Test
@DisplayName("세션이 없는 상태로 Account 정보를 요청하면 401 에러를 반환한다.")
void no_current_session_account_request() throws Exception {
//Act
final var actions = mockMvc.perform(get("/api/accounts/me")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON));
//Assert
actions
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("세션이 있는 상태로 Account 정보를 요청하면 200과 사용자 정보를 반환한다.")
void no_current_session_account_reqsuest() throws Exception {
//Arrange
var command = new GetSessionAccountCommand();
command.setEmail(loggedInEmail);
when(accountFacade.getSessionAccount(command)).thenReturn(
new AccountResponse(1L, loggedInEmail, "hello"));
//Act
final var actions = mockMvc.perform(get("/api/accounts/me")
.session(session)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON));
//Assert
actions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.email").isString())
.andExpect(jsonPath("$.nickname").isString());
}
@Test
@DisplayName("정상적인 이메일과 비밀번호를 보내 로그인에 성공하고 200을 반환한다.")
void login_success() throws Exception {
@@ -45,7 +103,7 @@ class AccountQueryApiTest {
doNothing().when(accountFacade).login(request);
//Act
var actions = mockMvc.perform(post("/api/accounts/login")
final var actions = mockMvc.perform(post("/api/accounts/login")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
@@ -135,7 +193,7 @@ class AccountQueryApiTest {
request.setPassword(args);
//Act
var actions = mockMvc.perform(post("/api/accounts/login")
final var actions = mockMvc.perform(post("/api/accounts/login")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))