diff --git a/build.gradle b/build.gradle index 8eff071..6fcc524 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' + id "org.sonarqube" version "2.7" } apply plugin: 'io.spring.dependency-management' @@ -32,5 +33,6 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/rest/api/advice/ExceptionAdvice.java b/src/main/java/com/rest/api/advice/ExceptionAdvice.java index c0b475a..7ed7d40 100644 --- a/src/main/java/com/rest/api/advice/ExceptionAdvice.java +++ b/src/main/java/com/rest/api/advice/ExceptionAdvice.java @@ -51,7 +51,7 @@ public class ExceptionAdvice { @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) - public CommonResult AccessDeniedException(HttpServletRequest request, AccessDeniedException e) { + public CommonResult accessDeniedException(HttpServletRequest request, AccessDeniedException e) { return responseService.getFailResult(Integer.valueOf(getMessage("accessDenied.code")), getMessage("accessDenied.msg")); } diff --git a/src/main/java/com/rest/api/config/MessageConfiguration.java b/src/main/java/com/rest/api/config/MessageConfiguration.java index ce36020..bff452a 100644 --- a/src/main/java/com/rest/api/config/MessageConfiguration.java +++ b/src/main/java/com/rest/api/config/MessageConfiguration.java @@ -1,6 +1,5 @@ package com.rest.api.config; -import lombok.extern.slf4j.Slf4j; import net.rakugakibox.util.YamlResourceBundle; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; @@ -14,7 +13,6 @@ import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import java.util.Locale; -import java.util.MissingResourceException; import java.util.ResourceBundle; @Configuration @@ -56,7 +54,7 @@ public class MessageConfiguration implements WebMvcConfigurer { // locale 정보에 따라 다른 yml 파일을 읽도록 처리 private static class YamlMessageSource extends ResourceBundleMessageSource { @Override - protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { + protected ResourceBundle doGetBundle(String basename, Locale locale) { return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE); } } diff --git a/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java b/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java index eaaa3cb..8b30ba8 100644 --- a/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java @@ -1,8 +1,6 @@ package com.rest.api.config.security; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @@ -17,12 +15,10 @@ import java.io.IOException; @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { - private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class); - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException { - RequestDispatcher dispatcher = request.getRequestDispatcher("/exception/accessdenied"); - dispatcher.forward(request, response); - } + RequestDispatcher dispatcher = request.getRequestDispatcher("/exception/accessdenied"); + dispatcher.forward(request, response); + } } diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index 923854e..ca31785 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -33,7 +33,6 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 .antMatchers("/*/signin", "/*/signup").permitAll() // 가입 및 인증 주소는 누구나 접근가능 .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 - .antMatchers("/*/users").hasRole("ADMIN") .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler()) diff --git a/src/main/java/com/rest/api/controller/HelloController.java b/src/main/java/com/rest/api/controller/HelloController.java index 22f68dd..f8241a0 100644 --- a/src/main/java/com/rest/api/controller/HelloController.java +++ b/src/main/java/com/rest/api/controller/HelloController.java @@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.ResponseBody; @Controller public class HelloController { + private static final String HELLO = "helloworld"; + @Setter @Getter public static class Hello { @@ -18,19 +20,19 @@ public class HelloController { @GetMapping(value = "/helloworld/string") @ResponseBody public String helloworldString() { - return "helloworld"; + return HELLO; } @GetMapping(value = "/helloworld/json") @ResponseBody public Hello helloworldJson() { Hello hello = new Hello(); - hello.message = "helloworld"; + hello.message = HELLO; return hello; } @GetMapping(value = "/helloworld/page") public String helloworld() { - return "helloworld"; + return HELLO; } } diff --git a/src/main/java/com/rest/api/controller/v1/SignController.java b/src/main/java/com/rest/api/controller/v1/SignController.java index 54b6cf6..82e2528 100644 --- a/src/main/java/com/rest/api/controller/v1/SignController.java +++ b/src/main/java/com/rest/api/controller/v1/SignController.java @@ -12,10 +12,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Collections; @@ -31,7 +28,7 @@ public class SignController { private final PasswordEncoder passwordEncoder; @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.") - @GetMapping(value = "/signin") + @PostMapping(value = "/signin") public SingleResult signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @ApiParam(value = "비밀번호", required = true) @RequestParam String password) { @@ -43,8 +40,8 @@ public class SignController { } @ApiOperation(value = "가입", notes = "회원가입을 한다.") - @GetMapping(value = "/signup") - public CommonResult signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, + @PostMapping(value = "/signup") + public CommonResult signup(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @ApiParam(value = "비밀번호", required = true) @RequestParam String password, @ApiParam(value = "이름", required = true) @RequestParam String name) { diff --git a/src/main/java/com/rest/api/controller/v1/UserController.java b/src/main/java/com/rest/api/controller/v1/UserController.java index 6e91ad3..c5c4541 100644 --- a/src/main/java/com/rest/api/controller/v1/UserController.java +++ b/src/main/java/com/rest/api/controller/v1/UserController.java @@ -33,11 +33,11 @@ public class UserController { } @ApiImplicitParams({ - @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "로그인 성공 후 access_token", required = false, dataType = "String", paramType = "header") + @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "로그인 성공 후 access_token", required = true, dataType = "String", paramType = "header") }) @ApiOperation(value = "회원 단건 조회", notes = "회원번호(msrl)로 회원을 조회한다") @GetMapping(value = "/user") - public SingleResult findUserById(@ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) { + public SingleResult findUser() { // SecurityContext에서 인증받은 회원의 정보를 얻어온다. Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String id = authentication.getName(); diff --git a/src/main/java/com/rest/api/entity/User.java b/src/main/java/com/rest/api/entity/User.java index e4c0f4c..83689e9 100644 --- a/src/main/java/com/rest/api/entity/User.java +++ b/src/main/java/com/rest/api/entity/User.java @@ -25,7 +25,7 @@ public class User implements UserDetails { @Id // pk @GeneratedValue(strategy = GenerationType.IDENTITY) private long msrl; - @Column(nullable = false, unique = true, length = 30) + @Column(nullable = false, unique = true, length = 50) private String uid; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @Column(nullable = false, length = 100) diff --git a/src/main/java/com/rest/api/service/ResponseService.java b/src/main/java/com/rest/api/service/ResponseService.java index fdc196b..a86f7cf 100644 --- a/src/main/java/com/rest/api/service/ResponseService.java +++ b/src/main/java/com/rest/api/service/ResponseService.java @@ -12,7 +12,7 @@ public class ResponseService { // enum으로 api 요청 결과에 대한 code, message를 정의합니다. public enum CommonResponse { - SUCCESS(0, "성공하였습니디."); + SUCCESS(0, "성공하였습니다."); int code; String msg; diff --git a/src/test/java/com/rest/api/SpringRestApiApplicationTests.java b/src/test/java/com/rest/api/SpringRestApiApplicationTests.java deleted file mode 100644 index 607846f..0000000 --- a/src/test/java/com/rest/api/SpringRestApiApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.rest.api; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class SpringRestApiApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/src/test/java/com/rest/api/controller/HelloControllerTest.java b/src/test/java/com/rest/api/controller/HelloControllerTest.java new file mode 100644 index 0000000..e03c475 --- /dev/null +++ b/src/test/java/com/rest/api/controller/HelloControllerTest.java @@ -0,0 +1,52 @@ +package com.rest.api.controller; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class HelloControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void helloworldString() throws Exception { + mockMvc.perform(get("/helloworld/string")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/plain;charset=UTF-8")) + .andExpect(content().string("helloworld")); + } + + @Test + public void helloworldJson() throws Exception { + mockMvc.perform(get("/helloworld/json")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=utf-8")) + .andExpect(jsonPath("$.message").value("helloworld")); + } + + @Test + public void helloworldPage() throws Exception { + mockMvc.perform(get("/helloworld/page")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/html;charset=UTF-8")) + .andExpect(view().name("helloworld")) + .andExpect(content().string("helloworld")); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java new file mode 100644 index 0000000..094ebf3 --- /dev/null +++ b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java @@ -0,0 +1,89 @@ +package com.rest.api.controller.v1; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +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; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class SignControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void signin() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "1234"); + mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()) + .andExpect(jsonPath("$.data").exists()); + } + + @Test + public void signinFail() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "12345"); + mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(-1001)) + .andExpect(jsonPath("$.msg").exists()); + } + + @Test + public void signup() throws Exception { + long epochTime = LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy_" + epochTime + "@naver.com"); + params.add("password", "12345"); + params.add("name", "happydaddy_" + epochTime); + mockMvc.perform(post("/v1/signup").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()); + } + + @Test + public void signupFail() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "12345"); + params.add("name", "happydaddy"); + mockMvc.perform(post("/v1/signup").params(params)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(-9999)); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java new file mode 100644 index 0000000..1f54444 --- /dev/null +++ b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java @@ -0,0 +1,119 @@ +package com.rest.api.controller.v1; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.json.JacksonJsonParser; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +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.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + private String token; + + @Before + public void setUp() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "1234"); + MvcResult result = mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()) + .andExpect(jsonPath("$.data").exists()) + .andReturn(); + + String resultString = result.getResponse().getContentAsString(); + JacksonJsonParser jsonParser = new JacksonJsonParser(); + token = jsonParser.parseMap(resultString).get("data").toString(); + } + + @Test + public void invalidToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users") + .header("X-AUTH-TOKEN", "XXXXXXXXXX")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/exception/entrypoint")); + } + + @Test + @WithMockUser(username = "mockUser", roles = {"ADMIN"}) // 가상의 Mock 유저 대입 + public void accessdenied() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users")) + //.header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/exception/accessdenied")); + } + + @Test + public void findAllUser() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.list").exists()); + } + + @Test + public void findUser() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/user") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").exists()); + } + + @Test + public void modify() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("msrl", "1"); + params.add("name", "행복전도사"); + mockMvc.perform(MockMvcRequestBuilders + .put("/v1/user") + .header("X-AUTH-TOKEN", token) + .params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + public void delete() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .delete("/v1/user/2") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/repo/UserJpaRepoTest.java b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java new file mode 100644 index 0000000..0d7a1a6 --- /dev/null +++ b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java @@ -0,0 +1,46 @@ +package com.rest.api.repo; + +import com.rest.api.entity.User; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Collections; +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@DataJpaTest +public class UserJpaRepoTest { + + @Autowired + private UserJpaRepo userJpaRepo; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + public void whenFindByUid_thenReturnUser() { + String uid = "angrydaddy@gmail.com"; + String name = "angrydaddy"; + // given + userJpaRepo.save(User.builder() + .uid(uid) + .password(passwordEncoder.encode("1234")) + .name(name) + .roles(Collections.singletonList("ROLE_USER")) + .build()); + // when + Optional user = userJpaRepo.findByUid(uid); + // then + assertNotNull(user);// user객체가 null이 아닌지 체크 + assertTrue(user.isPresent()); // user객체가 존재여부 true/false 체크 + assertEquals(user.get().getName(), name); // user객체의 name과 name변수 값이 같은지 체크 + assertThat(user.get().getName(), is(name)); // user객체의 name과 name변수 값이 같은지 체크 + } +} \ No newline at end of file