diff --git a/build.gradle b/build.gradle index bc6cf2d..f5e17b9 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' +apply plugin: 'findbugs' apply plugin: 'org.springframework.boot' version = '0.0.1-SNAPSHOT' @@ -22,6 +23,12 @@ repositories { mavenCentral() } +tasks.withType(FindBugs) { + reports { + xml.enabled false + html.enabled true + } +} dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa') diff --git a/src/main/java/io/spring/api/CommentsApi.java b/src/main/java/io/spring/api/CommentsApi.java index 95e8c96..1a3e2d3 100644 --- a/src/main/java/io/spring/api/CommentsApi.java +++ b/src/main/java/io/spring/api/CommentsApi.java @@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; -import javax.xml.ws.Response; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -67,7 +66,7 @@ public class CommentsApi { public ResponseEntity getComments(@PathVariable("slug") String slug, @AuthenticationPrincipal User user) { Article article = findArticle(slug); - List comments = commentQueryService.findByArticleSlug(article.getSlug(), user); + List comments = commentQueryService.findByArticleId(article.getId(), user); return ResponseEntity.ok(new HashMap() {{ put("comments", comments); }}); diff --git a/src/main/java/io/spring/api/CurrentUserApi.java b/src/main/java/io/spring/api/CurrentUserApi.java index 9c0dd02..54fa965 100644 --- a/src/main/java/io/spring/api/CurrentUserApi.java +++ b/src/main/java/io/spring/api/CurrentUserApi.java @@ -1,6 +1,7 @@ package io.spring.api; import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.api.exception.InvalidRequestException; import io.spring.application.user.UserQueryService; import io.spring.application.user.UserWithToken; import io.spring.core.user.User; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @RestController @RequestMapping(path = "/user") @@ -43,9 +45,13 @@ public class CurrentUserApi { @PutMapping public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser, - @RequestHeader(value = "Authorization") String authorization, @Valid @RequestBody UpdateUserParam updateUserParam, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + throw new InvalidRequestException(bindingResult); + } + checkUniquenessOfUsernameAndEmail(currentUser, updateUserParam, bindingResult); + currentUser.update( updateUserParam.getEmail(), updateUserParam.getUsername(), @@ -53,7 +59,27 @@ public class CurrentUserApi { updateUserParam.getBio(), updateUserParam.getImage()); userRepository.save(currentUser); - return ResponseEntity.ok(userResponse(userQueryService.fetchCurrentUser(currentUser.getUsername(), authorization.split(" ")[1]))); + return ResponseEntity.ok(userResponse(userQueryService.fetchNewAuthenticatedUser(currentUser.getUsername()))); + } + + private void checkUniquenessOfUsernameAndEmail(User currentUser, UpdateUserParam updateUserParam, BindingResult bindingResult) { + if (!"".equals(updateUserParam.getUsername())) { + Optional byUsername = userRepository.findByUsername(updateUserParam.getUsername()); + if (byUsername.isPresent() && !byUsername.get().equals(currentUser)) { + bindingResult.rejectValue("username", "DUPLICATED", "username already exist"); + } + } + + if (!"".equals(updateUserParam.getEmail())) { + Optional byEmail = userRepository.findByEmail(updateUserParam.getEmail()); + if (byEmail.isPresent() && !byEmail.get().equals(currentUser)) { + bindingResult.rejectValue("email", "DUPLICATED", "email already exist"); + } + } + + if (bindingResult.hasErrors()) { + throw new InvalidRequestException(bindingResult); + } } private Map userResponse(UserWithToken userWithToken) { diff --git a/src/main/java/io/spring/api/security/CORSConfig.java b/src/main/java/io/spring/api/security/CORSConfig.java new file mode 100644 index 0000000..209dec5 --- /dev/null +++ b/src/main/java/io/spring/api/security/CORSConfig.java @@ -0,0 +1,25 @@ +package io.spring.api.security; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CORSConfig { + @Bean + public FilterRegistrationBean corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); + bean.setOrder(0); + return bean; + } +} diff --git a/src/main/java/io/spring/api/security/JwtTokenFilter.java b/src/main/java/io/spring/api/security/JwtTokenFilter.java index e29b5fd..40073e0 100644 --- a/src/main/java/io/spring/api/security/JwtTokenFilter.java +++ b/src/main/java/io/spring/api/security/JwtTokenFilter.java @@ -30,16 +30,17 @@ public class JwtTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { getTokenString(request.getHeader(header)).ifPresent(token -> { - jwtService.getSubFromToken(token).ifPresent(username -> { + jwtService.getSubFromToken(token).ifPresent(id -> { if (SecurityContextHolder.getContext().getAuthentication() == null) { - User user = userRepository.findByUsername(username).get(); - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - user, - null, - Collections.emptyList() - ); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); + userRepository.findById(id).ifPresent(user -> { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + user, + null, + Collections.emptyList() + ); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + }); } }); }); diff --git a/src/main/java/io/spring/api/security/WebSecurityConfig.java b/src/main/java/io/spring/api/security/WebSecurityConfig.java index 361fd32..58ba177 100644 --- a/src/main/java/io/spring/api/security/WebSecurityConfig.java +++ b/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -25,9 +25,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() + .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers(HttpMethod.GET, "/articles/feed").authenticated() .antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll() - .antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**").permitAll() + .antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/io/spring/application/comment/CommentQueryService.java b/src/main/java/io/spring/application/comment/CommentQueryService.java index 3a754fd..cff0602 100644 --- a/src/main/java/io/spring/application/comment/CommentQueryService.java +++ b/src/main/java/io/spring/application/comment/CommentQueryService.java @@ -4,9 +4,10 @@ import io.spring.application.profile.UserRelationshipQueryService; import io.spring.core.user.User; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; @Service public class CommentQueryService { @@ -31,7 +32,16 @@ public class CommentQueryService { return Optional.ofNullable(commentData); } - public List findByArticleSlug(String slug, User user) { - return new ArrayList<>(); + public List findByArticleId(String articleId, User user) { + List comments = commentReadService.findByArticleId(articleId); + if (comments.size() > 0) { + Set followingAuthors = userRelationshipQueryService.followingAuthors(user.getId(), comments.stream().map(commentData -> commentData.getProfileData().getId()).collect(Collectors.toList())); + comments.forEach(commentData -> { + if (followingAuthors.contains(commentData.getProfileData().getId())) { + commentData.getProfileData().setFollowing(true); + } + }); + } + return comments; } } diff --git a/src/main/java/io/spring/application/comment/CommentReadService.java b/src/main/java/io/spring/application/comment/CommentReadService.java index 5b2a4b8..9527bd8 100644 --- a/src/main/java/io/spring/application/comment/CommentReadService.java +++ b/src/main/java/io/spring/application/comment/CommentReadService.java @@ -1,10 +1,15 @@ package io.spring.application.comment; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Component; +import java.util.List; + @Component @Mapper public interface CommentReadService { - CommentData findById(String id); + CommentData findById(@Param("id") String id); + + List findByArticleId(@Param("articleId") String articleId); } diff --git a/src/main/java/io/spring/core/article/Article.java b/src/main/java/io/spring/core/article/Article.java index aed678f..3762908 100644 --- a/src/main/java/io/spring/core/article/Article.java +++ b/src/main/java/io/spring/core/article/Article.java @@ -53,6 +53,7 @@ public class Article { if (!"".equals(body)) { this.body = body; } + this.updatedAt = new DateTime(); } private String toSlug(String title) { diff --git a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java index add8cbd..f8a58f9 100644 --- a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java +++ b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java @@ -28,7 +28,7 @@ public class DefaultJwtService implements JwtService { @Override public String toToken(UserData userData) { return Jwts.builder() - .setSubject(userData.getUsername()) + .setSubject(userData.getId()) .setExpiration(expireTimeFromNow()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); diff --git a/src/main/resources/mapper/ArticleFavoritesQueryService.xml b/src/main/resources/mapper/ArticleFavoritesQueryService.xml index c3db49f..8cafc77 100644 --- a/src/main/resources/mapper/ArticleFavoritesQueryService.xml +++ b/src/main/resources/mapper/ArticleFavoritesQueryService.xml @@ -8,7 +8,7 @@ select count(1) from article_favorites where article_id = #{articleId} + SELECT - C.id commentId, - C.body commentBody, - C.created_at commentCreatedAt, - + C.id commentId, + C.body commentBody, + C.created_at commentCreatedAt, + from comments C left join users U on C.user_id = U.id + + + + diff --git a/src/test/java/io/spring/api/CommentsApiTest.java b/src/test/java/io/spring/api/CommentsApiTest.java index 3303e64..fd158c6 100644 --- a/src/test/java/io/spring/api/CommentsApiTest.java +++ b/src/test/java/io/spring/api/CommentsApiTest.java @@ -115,7 +115,7 @@ public class CommentsApiTest extends TestWithCurrentUser { @Test public void should_get_comments_of_article_success() throws Exception { - when(commentQueryService.findByArticleSlug(anyString(), eq(null))).thenReturn(Arrays.asList(commentData)); + when(commentQueryService.findByArticleId(anyString(), eq(null))).thenReturn(Arrays.asList(commentData)); RestAssured.when() .get("/articles/{slug}/comments", article.getSlug()) .prettyPeek() diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java index b617881..e539f39 100644 --- a/src/test/java/io/spring/api/CurrentUserApiTest.java +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -1,20 +1,29 @@ package io.spring.api; import io.restassured.RestAssured; +import io.spring.application.JwtService; +import io.spring.application.user.UserData; +import io.spring.core.user.User; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -24,16 +33,9 @@ public class CurrentUserApiTest extends TestWithCurrentUser { @LocalServerPort private int port; - protected String email; - protected String username; - protected String defaultAvatar; - @Before public void setUp() throws Exception { RestAssured.port = port; - email = "john@jacob.com"; - username = "johnjacob"; - defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; userFixture(); } @@ -51,7 +53,7 @@ public class CurrentUserApiTest extends TestWithCurrentUser { .body("user.email", equalTo(email)) .body("user.username", equalTo(username)) .body("user.bio", equalTo("")) - .body("user.image", equalTo("https://static.productionready.io/images/smiley-cyrus.jpg")) + .body("user.image", equalTo(defaultAvatar)) .body("user.token", equalTo(token)); } @@ -81,14 +83,18 @@ public class CurrentUserApiTest extends TestWithCurrentUser { public void should_update_current_user_profile() throws Exception { String newEmail = "newemail@example.com"; String newBio = "updated"; + String newUsername = "newusernamee"; Map param = new HashMap() {{ put("user", new HashMap() {{ put("email", newEmail); put("bio", newBio); + put("username", newUsername); }}); }}; + when(userReadService.findByUsername(eq(newUsername))).thenReturn(new UserData(user.getId(), newEmail, newUsername, newBio, user.getImage())); + given() .contentType("application/json") .header("Authorization", "Token " + token) @@ -96,11 +102,44 @@ public class CurrentUserApiTest extends TestWithCurrentUser { .when() .put("/user") .then() - .statusCode(200); + .statusCode(200) + .body("user.token", not(token)); + } - assertThat(user.getEmail(), is(newEmail)); - assertThat(user.getBio(), is(newBio)); - assertThat(user.getImage(), is(defaultAvatar)); + @Test + public void should_get_error_if_email_exists_when_update_user_profile() throws Exception { + String newEmail = "newemail@example.com"; + String newBio = "updated"; + String newUsername = "newusernamee"; + + Map param = prepareUpdateParam(newEmail, newBio, newUsername); + + when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.of(new User(newEmail, "username", "123", "", ""))); + when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); + + when(userReadService.findByUsername(eq(newUsername))).thenReturn(new UserData(user.getId(), newEmail, newUsername, newBio, user.getImage())); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .put("/user") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("email already exist")); + + } + + private HashMap prepareUpdateParam(final String newEmail, final String newBio, final String newUsername) { + return new HashMap() {{ + put("user", new HashMap() {{ + put("email", newEmail); + put("bio", newBio); + put("username", newUsername); + }}); + }}; } @Test diff --git a/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java index 07e9c14..df5a051 100644 --- a/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java +++ b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java @@ -1,9 +1,13 @@ package io.spring.application.comment; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; import io.spring.core.comment.Comment; import io.spring.core.comment.CommentRepository; +import io.spring.core.user.FollowRelation; import io.spring.core.user.User; import io.spring.core.user.UserRepository; +import io.spring.infrastructure.article.MyBatisArticleRepository; import io.spring.infrastructure.comment.MyBatisCommentRepository; import io.spring.infrastructure.user.MyBatisUserRepository; import org.junit.Before; @@ -14,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit4.SpringRunner; +import java.util.List; import java.util.Optional; import static org.hamcrest.CoreMatchers.is; @@ -21,7 +26,7 @@ import static org.junit.Assert.*; @MybatisTest @RunWith(SpringRunner.class) -@Import({MyBatisCommentRepository.class, MyBatisUserRepository.class, CommentQueryService.class}) +@Import({MyBatisCommentRepository.class, MyBatisUserRepository.class, CommentQueryService.class, MyBatisArticleRepository.class}) public class CommentQueryServiceTest { @Autowired private CommentRepository commentRepository; @@ -31,6 +36,10 @@ public class CommentQueryServiceTest { @Autowired private CommentQueryService commentQueryService; + + @Autowired + private ArticleRepository articleRepository; + private User user; @Before @@ -49,4 +58,23 @@ public class CommentQueryServiceTest { CommentData commentData = optional.get(); assertThat(commentData.getProfileData().getUsername(), is(user.getUsername())); } + + @Test + public void should_read_comments_of_article() throws Exception { + Article article = new Article("title", "desc", "body", new String[]{"java"}, user.getId()); + articleRepository.save(article); + + User user2 = new User("user2@email.com", "user2", "123", "", ""); + userRepository.save(user2); + userRepository.saveRelation(new FollowRelation(user.getId(), user2.getId())); + + Comment comment1 = new Comment("content1", user.getId(), article.getId()); + commentRepository.save(comment1); + Comment comment2 = new Comment("content2", user2.getId(), article.getId()); + commentRepository.save(comment2); + + List comments = commentQueryService.findByArticleId(article.getId(), user); + assertThat(comments.size(), is(2)); + + } } \ No newline at end of file diff --git a/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java index f21ce56..1e6c2c2 100644 --- a/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java +++ b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java @@ -2,8 +2,6 @@ package io.spring.application.tag; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; -import io.spring.core.article.Tag; -import io.spring.infrastructure.article.ArticleMapper; import io.spring.infrastructure.article.MyBatisArticleRepository; import org.junit.Test; import org.junit.runner.RunWith;