This commit is contained in:
aisensiy 2017-08-18 12:09:07 +08:00
parent d6bf680a97
commit c3029aa636
16 changed files with 189 additions and 41 deletions

View File

@ -13,6 +13,7 @@ buildscript {
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'eclipse' apply plugin: 'eclipse'
apply plugin: 'idea' apply plugin: 'idea'
apply plugin: 'findbugs'
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
@ -22,6 +23,12 @@ repositories {
mavenCentral() mavenCentral()
} }
tasks.withType(FindBugs) {
reports {
xml.enabled false
html.enabled true
}
}
dependencies { dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa') compile('org.springframework.boot:spring-boot-starter-data-jpa')

View File

@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid; import javax.validation.Valid;
import javax.xml.ws.Response;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -67,7 +66,7 @@ public class CommentsApi {
public ResponseEntity getComments(@PathVariable("slug") String slug, public ResponseEntity getComments(@PathVariable("slug") String slug,
@AuthenticationPrincipal User user) { @AuthenticationPrincipal User user) {
Article article = findArticle(slug); Article article = findArticle(slug);
List<CommentData> comments = commentQueryService.findByArticleSlug(article.getSlug(), user); List<CommentData> comments = commentQueryService.findByArticleId(article.getId(), user);
return ResponseEntity.ok(new HashMap<String, Object>() {{ return ResponseEntity.ok(new HashMap<String, Object>() {{
put("comments", comments); put("comments", comments);
}}); }});

View File

@ -1,6 +1,7 @@
package io.spring.api; package io.spring.api;
import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.annotation.JsonRootName;
import io.spring.api.exception.InvalidRequestException;
import io.spring.application.user.UserQueryService; import io.spring.application.user.UserQueryService;
import io.spring.application.user.UserWithToken; import io.spring.application.user.UserWithToken;
import io.spring.core.user.User; import io.spring.core.user.User;
@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
@RestController @RestController
@RequestMapping(path = "/user") @RequestMapping(path = "/user")
@ -43,9 +45,13 @@ public class CurrentUserApi {
@PutMapping @PutMapping
public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser, public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser,
@RequestHeader(value = "Authorization") String authorization,
@Valid @RequestBody UpdateUserParam updateUserParam, @Valid @RequestBody UpdateUserParam updateUserParam,
BindingResult bindingResult) { BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
throw new InvalidRequestException(bindingResult);
}
checkUniquenessOfUsernameAndEmail(currentUser, updateUserParam, bindingResult);
currentUser.update( currentUser.update(
updateUserParam.getEmail(), updateUserParam.getEmail(),
updateUserParam.getUsername(), updateUserParam.getUsername(),
@ -53,7 +59,27 @@ public class CurrentUserApi {
updateUserParam.getBio(), updateUserParam.getBio(),
updateUserParam.getImage()); updateUserParam.getImage());
userRepository.save(currentUser); 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<User> byUsername = userRepository.findByUsername(updateUserParam.getUsername());
if (byUsername.isPresent() && !byUsername.get().equals(currentUser)) {
bindingResult.rejectValue("username", "DUPLICATED", "username already exist");
}
}
if (!"".equals(updateUserParam.getEmail())) {
Optional<User> 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<String, Object> userResponse(UserWithToken userWithToken) { private Map<String, Object> userResponse(UserWithToken userWithToken) {

View File

@ -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;
}
}

View File

@ -30,9 +30,9 @@ public class JwtTokenFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
getTokenString(request.getHeader(header)).ifPresent(token -> { getTokenString(request.getHeader(header)).ifPresent(token -> {
jwtService.getSubFromToken(token).ifPresent(username -> { jwtService.getSubFromToken(token).ifPresent(id -> {
if (SecurityContextHolder.getContext().getAuthentication() == null) { if (SecurityContextHolder.getContext().getAuthentication() == null) {
User user = userRepository.findByUsername(username).get(); userRepository.findById(id).ifPresent(user -> {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user, user,
null, null,
@ -40,6 +40,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
); );
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticationToken);
});
} }
}); });
}); });

View File

@ -25,9 +25,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.and() .and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers(HttpMethod.GET, "/articles/feed").authenticated() .antMatchers(HttpMethod.GET, "/articles/feed").authenticated()
.antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll() .antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll()
.antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**").permitAll() .antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags").permitAll()
.anyRequest().authenticated(); .anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

View File

@ -4,9 +4,10 @@ import io.spring.application.profile.UserRelationshipQueryService;
import io.spring.core.user.User; import io.spring.core.user.User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service @Service
public class CommentQueryService { public class CommentQueryService {
@ -31,7 +32,16 @@ public class CommentQueryService {
return Optional.ofNullable(commentData); return Optional.ofNullable(commentData);
} }
public List<CommentData> findByArticleSlug(String slug, User user) { public List<CommentData> findByArticleId(String articleId, User user) {
return new ArrayList<>(); List<CommentData> comments = commentReadService.findByArticleId(articleId);
if (comments.size() > 0) {
Set<String> 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;
} }
} }

View File

@ -1,10 +1,15 @@
package io.spring.application.comment; package io.spring.application.comment;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
@Component @Component
@Mapper @Mapper
public interface CommentReadService { public interface CommentReadService {
CommentData findById(String id); CommentData findById(@Param("id") String id);
List<CommentData> findByArticleId(@Param("articleId") String articleId);
} }

View File

@ -53,6 +53,7 @@ public class Article {
if (!"".equals(body)) { if (!"".equals(body)) {
this.body = body; this.body = body;
} }
this.updatedAt = new DateTime();
} }
private String toSlug(String title) { private String toSlug(String title) {

View File

@ -28,7 +28,7 @@ public class DefaultJwtService implements JwtService {
@Override @Override
public String toToken(UserData userData) { public String toToken(UserData userData) {
return Jwts.builder() return Jwts.builder()
.setSubject(userData.getUsername()) .setSubject(userData.getId())
.setExpiration(expireTimeFromNow()) .setExpiration(expireTimeFromNow())
.signWith(SignatureAlgorithm.HS512, secret) .signWith(SignatureAlgorithm.HS512, secret)
.compact(); .compact();

View File

@ -8,7 +8,7 @@
select count(1) from article_favorites where article_id = #{articleId} select count(1) from article_favorites where article_id = #{articleId}
</select> </select>
<select id="articlesFavoriteCount" resultMap="favoriteCount"> <select id="articlesFavoriteCount" resultMap="favoriteCount">
select A.id, count(A.id) as favoriteCount from articles A select A.id, count(AF.user_id) as favoriteCount from articles A
left join article_favorites AF on A.id = AF.article_id left join article_favorites AF on A.id = AF.article_id
where id in where id in
<foreach collection="ids" item="item" separator="," open="(" close=")"> <foreach collection="ids" item="item" separator="," open="(" close=")">

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.spring.application.comment.CommentReadService"> <mapper namespace="io.spring.application.comment.CommentReadService">
<select id="findById" resultMap="commentData"> <sql id="selectCommentData">
SELECT SELECT
C.id commentId, C.id commentId,
C.body commentBody, C.body commentBody,
@ -10,8 +10,16 @@
from comments C from comments C
left join users U left join users U
on C.user_id = U.id on C.user_id = U.id
</sql>
<select id="findById" resultMap="commentData">
<include refid="selectCommentData"/>
where C.id = #{id} where C.id = #{id}
</select> </select>
<select id="findByArticleId" resultMap="commentData">
<include refid="selectCommentData"/>
where C.article_id = #{articleId}
</select>
<resultMap id="commentData" type="io.spring.application.comment.CommentData"> <resultMap id="commentData" type="io.spring.application.comment.CommentData">
<id column="commentId" property="id"/> <id column="commentId" property="id"/>

View File

@ -115,7 +115,7 @@ public class CommentsApiTest extends TestWithCurrentUser {
@Test @Test
public void should_get_comments_of_article_success() throws Exception { 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() RestAssured.when()
.get("/articles/{slug}/comments", article.getSlug()) .get("/articles/{slug}/comments", article.getSlug())
.prettyPeek() .prettyPeek()

View File

@ -1,20 +1,29 @@
package io.spring.api; package io.spring.api;
import io.restassured.RestAssured; 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.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo; 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; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT) @SpringBootTest(webEnvironment = RANDOM_PORT)
@ -24,16 +33,9 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
@LocalServerPort @LocalServerPort
private int port; private int port;
protected String email;
protected String username;
protected String defaultAvatar;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
RestAssured.port = port; RestAssured.port = port;
email = "john@jacob.com";
username = "johnjacob";
defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg";
userFixture(); userFixture();
} }
@ -51,7 +53,7 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
.body("user.email", equalTo(email)) .body("user.email", equalTo(email))
.body("user.username", equalTo(username)) .body("user.username", equalTo(username))
.body("user.bio", equalTo("")) .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)); .body("user.token", equalTo(token));
} }
@ -81,14 +83,18 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
public void should_update_current_user_profile() throws Exception { public void should_update_current_user_profile() throws Exception {
String newEmail = "newemail@example.com"; String newEmail = "newemail@example.com";
String newBio = "updated"; String newBio = "updated";
String newUsername = "newusernamee";
Map<String, Object> param = new HashMap<String, Object>() {{ Map<String, Object> param = new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{ put("user", new HashMap<String, Object>() {{
put("email", newEmail); put("email", newEmail);
put("bio", newBio); put("bio", newBio);
put("username", newUsername);
}}); }});
}}; }};
when(userReadService.findByUsername(eq(newUsername))).thenReturn(new UserData(user.getId(), newEmail, newUsername, newBio, user.getImage()));
given() given()
.contentType("application/json") .contentType("application/json")
.header("Authorization", "Token " + token) .header("Authorization", "Token " + token)
@ -96,11 +102,44 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
.when() .when()
.put("/user") .put("/user")
.then() .then()
.statusCode(200); .statusCode(200)
.body("user.token", not(token));
}
assertThat(user.getEmail(), is(newEmail)); @Test
assertThat(user.getBio(), is(newBio)); public void should_get_error_if_email_exists_when_update_user_profile() throws Exception {
assertThat(user.getImage(), is(defaultAvatar)); String newEmail = "newemail@example.com";
String newBio = "updated";
String newUsername = "newusernamee";
Map<String, Object> 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<String, Object> prepareUpdateParam(final String newEmail, final String newBio, final String newUsername) {
return new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
put("email", newEmail);
put("bio", newBio);
put("username", newUsername);
}});
}};
} }
@Test @Test

View File

@ -1,9 +1,13 @@
package io.spring.application.comment; 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.Comment;
import io.spring.core.comment.CommentRepository; import io.spring.core.comment.CommentRepository;
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User; import io.spring.core.user.User;
import io.spring.core.user.UserRepository; import io.spring.core.user.UserRepository;
import io.spring.infrastructure.article.MyBatisArticleRepository;
import io.spring.infrastructure.comment.MyBatisCommentRepository; import io.spring.infrastructure.comment.MyBatisCommentRepository;
import io.spring.infrastructure.user.MyBatisUserRepository; import io.spring.infrastructure.user.MyBatisUserRepository;
import org.junit.Before; import org.junit.Before;
@ -14,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
@ -21,7 +26,7 @@ import static org.junit.Assert.*;
@MybatisTest @MybatisTest
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@Import({MyBatisCommentRepository.class, MyBatisUserRepository.class, CommentQueryService.class}) @Import({MyBatisCommentRepository.class, MyBatisUserRepository.class, CommentQueryService.class, MyBatisArticleRepository.class})
public class CommentQueryServiceTest { public class CommentQueryServiceTest {
@Autowired @Autowired
private CommentRepository commentRepository; private CommentRepository commentRepository;
@ -31,6 +36,10 @@ public class CommentQueryServiceTest {
@Autowired @Autowired
private CommentQueryService commentQueryService; private CommentQueryService commentQueryService;
@Autowired
private ArticleRepository articleRepository;
private User user; private User user;
@Before @Before
@ -49,4 +58,23 @@ public class CommentQueryServiceTest {
CommentData commentData = optional.get(); CommentData commentData = optional.get();
assertThat(commentData.getProfileData().getUsername(), is(user.getUsername())); 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<CommentData> comments = commentQueryService.findByArticleId(article.getId(), user);
assertThat(comments.size(), is(2));
}
} }

View File

@ -2,8 +2,6 @@ package io.spring.application.tag;
import io.spring.core.article.Article; import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository; 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 io.spring.infrastructure.article.MyBatisArticleRepository;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;