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: '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')

View File

@ -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<CommentData> comments = commentQueryService.findByArticleSlug(article.getSlug(), user);
List<CommentData> comments = commentQueryService.findByArticleId(article.getId(), user);
return ResponseEntity.ok(new HashMap<String, Object>() {{
put("comments", comments);
}});

View File

@ -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<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) {

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
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();
userRepository.findById(id).ifPresent(user -> {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user,
null,
@ -40,6 +40,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
});
}
});
});

View File

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

View File

@ -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<CommentData> findByArticleSlug(String slug, User user) {
return new ArrayList<>();
public List<CommentData> findByArticleId(String articleId, User user) {
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;
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<CommentData> findByArticleId(@Param("articleId") String articleId);
}

View File

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

View File

@ -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();

View File

@ -8,7 +8,7 @@
select count(1) from article_favorites where article_id = #{articleId}
</select>
<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
where id in
<foreach collection="ids" item="item" separator="," open="(" close=")">

View File

@ -1,7 +1,7 @@
<?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" >
<mapper namespace="io.spring.application.comment.CommentReadService">
<select id="findById" resultMap="commentData">
<sql id="selectCommentData">
SELECT
C.id commentId,
C.body commentBody,
@ -10,8 +10,16 @@
from comments C
left join users U
on C.user_id = U.id
</sql>
<select id="findById" resultMap="commentData">
<include refid="selectCommentData"/>
where C.id = #{id}
</select>
<select id="findByArticleId" resultMap="commentData">
<include refid="selectCommentData"/>
where C.article_id = #{articleId}
</select>
<resultMap id="commentData" type="io.spring.application.comment.CommentData">
<id column="commentId" property="id"/>

View File

@ -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()

View File

@ -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<String, Object> param = new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
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<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

View File

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