diff --git a/build.gradle b/build.gradle index af0950f..bc6cf2d 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('io.jsonwebtoken:jjwt:0.7.0') compile('org.springframework.boot:spring-boot-starter-security') + compile('joda-time:joda-time:2.9.9') compileOnly('org.projectlombok:lombok') runtime('com.h2database:h2') testCompile 'io.rest-assured:rest-assured:3.0.2' diff --git a/src/main/java/io/spring/api/ArticlesApi.java b/src/main/java/io/spring/api/ArticlesApi.java new file mode 100644 index 0000000..1a7bb00 --- /dev/null +++ b/src/main/java/io/spring/api/ArticlesApi.java @@ -0,0 +1,67 @@ +package io.spring.api; + +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.api.exception.InvalidRequestException; +import io.spring.application.article.ArticleQueryService; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.user.User; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequestMapping(path = "/articles") +public class ArticlesApi { + private ArticleRepository articleRepository; + private ArticleQueryService articleQueryService; + + @Autowired + public ArticlesApi(ArticleRepository articleRepository, ArticleQueryService articleQueryService) { + this.articleRepository = articleRepository; + this.articleQueryService = articleQueryService; + } + + @PostMapping + public ResponseEntity createArticle(@Valid @RequestBody NewArticleParam newArticleParam, + BindingResult bindingResult, + @AuthenticationPrincipal User user) { + if (bindingResult.hasErrors()) { + throw new InvalidRequestException(bindingResult); + } + + Article article = new Article( + articleRepository.toSlug( + newArticleParam.getTitle()), + newArticleParam.getTitle(), + newArticleParam.getDescription(), + newArticleParam.getBody(), + newArticleParam.getTagList(), + user.getId()); + articleRepository.save(article); + return ResponseEntity.ok(articleQueryService.findById(article.getId(), user).get()); + } +} + +@Getter +@JsonRootName("article") +@NoArgsConstructor +class NewArticleParam { + @NotBlank(message = "can't be empty") + private String title; + @NotBlank(message = "can't be empty") + private String description; + @NotBlank(message = "can't be empty") + private String body; + private String[] tagList; +} \ No newline at end of file diff --git a/src/main/java/io/spring/application/article/ArticleData.java b/src/main/java/io/spring/application/article/ArticleData.java new file mode 100644 index 0000000..9048c74 --- /dev/null +++ b/src/main/java/io/spring/application/article/ArticleData.java @@ -0,0 +1,31 @@ +package io.spring.application.article; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.application.profile.ProfileData; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonRootName("article") +public class ArticleData { + private String id; + private String slug; + private String title; + private String description; + private String body; + private boolean favorited; + private int favoritesCount; + private DateTime createdAt; + private DateTime updatedAt; + private List tagList; + @JsonProperty("author") + private ProfileData profileData; +} + diff --git a/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java b/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java new file mode 100644 index 0000000..0d89c35 --- /dev/null +++ b/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java @@ -0,0 +1,13 @@ +package io.spring.application.article; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Mapper +@Component +public interface ArticleFavoritesQueryService { + boolean isUserFavorite(@Param("userId") String userId, @Param("articleId") String articleId); + + int articleFavoriteCount(@Param("articleId") String articleId); +} diff --git a/src/main/java/io/spring/application/article/ArticleQueryService.java b/src/main/java/io/spring/application/article/ArticleQueryService.java new file mode 100644 index 0000000..1ba022f --- /dev/null +++ b/src/main/java/io/spring/application/article/ArticleQueryService.java @@ -0,0 +1,39 @@ +package io.spring.application.article; + +import io.spring.application.profile.UserRelationshipQueryService; +import io.spring.core.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ArticleQueryService { + private ArticleReadService articleReadService; + private UserRelationshipQueryService userRelationshipQueryService; + private ArticleFavoritesQueryService articleFavoritesQueryService; + + @Autowired + public ArticleQueryService(ArticleReadService articleReadService, + UserRelationshipQueryService userRelationshipQueryService, + ArticleFavoritesQueryService articleFavoritesQueryService) { + this.articleReadService = articleReadService; + this.userRelationshipQueryService = userRelationshipQueryService; + this.articleFavoritesQueryService = articleFavoritesQueryService; + } + + public Optional findById(String id, User user) { + ArticleData articleData = articleReadService.ofId(id); + if (articleData == null) { + return Optional.empty(); + } else { + articleData.setFavorited(articleFavoritesQueryService.isUserFavorite(user.getId(), id)); + articleData.setFavoritesCount(articleFavoritesQueryService.articleFavoriteCount(id)); + articleData.getProfileData().setFollowing( + userRelationshipQueryService.isUserFollowing( + user.getId(), + articleData.getProfileData().getId())); + return Optional.of(articleData); + } + } +} diff --git a/src/main/java/io/spring/application/article/ArticleReadService.java b/src/main/java/io/spring/application/article/ArticleReadService.java new file mode 100644 index 0000000..8291aca --- /dev/null +++ b/src/main/java/io/spring/application/article/ArticleReadService.java @@ -0,0 +1,11 @@ +package io.spring.application.article; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface ArticleReadService { + ArticleData ofId(@Param("id") String id); +} diff --git a/src/main/java/io/spring/application/profile/ProfileData.java b/src/main/java/io/spring/application/profile/ProfileData.java new file mode 100644 index 0000000..97e2a7d --- /dev/null +++ b/src/main/java/io/spring/application/profile/ProfileData.java @@ -0,0 +1,18 @@ +package io.spring.application.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProfileData { + @JsonIgnore + private String id; + private String username; + private String bio; + private String image; + private boolean following; +} diff --git a/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java b/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java new file mode 100644 index 0000000..34895a0 --- /dev/null +++ b/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java @@ -0,0 +1,11 @@ +package io.spring.application.profile; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface UserRelationshipQueryService { + boolean isUserFollowing(@Param("userId") String userId, @Param("anotherUserId") String anotherUserId); +} diff --git a/src/main/java/io/spring/application/user/UserData.java b/src/main/java/io/spring/application/user/UserData.java index 0c171f7..04db5c8 100644 --- a/src/main/java/io/spring/application/user/UserData.java +++ b/src/main/java/io/spring/application/user/UserData.java @@ -11,10 +11,8 @@ import javax.persistence.Id; @Data @NoArgsConstructor @AllArgsConstructor -@Entity @JsonRootName("user") public class UserData { - @Id private String id; private String email; private String username; diff --git a/src/main/java/io/spring/application/user/UserQueryService.java b/src/main/java/io/spring/application/user/UserQueryService.java index 90540a3..585c3be 100644 --- a/src/main/java/io/spring/application/user/UserQueryService.java +++ b/src/main/java/io/spring/application/user/UserQueryService.java @@ -16,12 +16,12 @@ public class UserQueryService { } public UserWithToken fetchNewAuthenticatedUser(String username) { - UserData userData = userReadService.findOne(username); + UserData userData = userReadService.findByUsername(username); return new UserWithToken(userData, jwtService.toToken(userData)); } public UserWithToken fetchCurrentUser(String username, String token) { - return new UserWithToken(userReadService.findOne(username), token); + return new UserWithToken(userReadService.findByUsername(username), token); } } diff --git a/src/main/java/io/spring/application/user/UserReadService.java b/src/main/java/io/spring/application/user/UserReadService.java index 37306b1..08998b9 100644 --- a/src/main/java/io/spring/application/user/UserReadService.java +++ b/src/main/java/io/spring/application/user/UserReadService.java @@ -1,8 +1,13 @@ package io.spring.application.user; -import org.springframework.data.repository.CrudRepository; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; -public interface UserReadService extends CrudRepository { +@Component +@Mapper +public interface UserReadService { + UserData findByUsername(@Param("username") String username); } diff --git a/src/main/java/io/spring/core/article/Article.java b/src/main/java/io/spring/core/article/Article.java new file mode 100644 index 0000000..70adc9c --- /dev/null +++ b/src/main/java/io/spring/core/article/Article.java @@ -0,0 +1,41 @@ +package io.spring.core.article; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = {"id"}) +public class Article { + private String userId; + private String id; + private String slug; + private String title; + private String description; + private String body; + private List tags; + private DateTime createdAt; + private DateTime updatedAt; + + public Article(String slug, String title, String description, String body, String[] tagList, String userId) { + this.id = UUID.randomUUID().toString(); + this.slug = slug; + this.title = title; + this.description = description; + this.body = body; + this.tags = Arrays.stream(tagList).collect(toSet()).stream().map(Tag::new).collect(toList()); + this.userId = userId; + this.createdAt = new DateTime(); + this.updatedAt = new DateTime(); + } + +} diff --git a/src/main/java/io/spring/core/article/ArticleRepository.java b/src/main/java/io/spring/core/article/ArticleRepository.java new file mode 100644 index 0000000..375f972 --- /dev/null +++ b/src/main/java/io/spring/core/article/ArticleRepository.java @@ -0,0 +1,11 @@ +package io.spring.core.article; + +import java.util.Optional; + +public interface ArticleRepository { + String toSlug(String title); + + void save(Article article); + + Optional
findById(String id); +} diff --git a/src/main/java/io/spring/core/article/Tag.java b/src/main/java/io/spring/core/article/Tag.java new file mode 100644 index 0000000..adefda5 --- /dev/null +++ b/src/main/java/io/spring/core/article/Tag.java @@ -0,0 +1,20 @@ +package io.spring.core.article; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(of = "name") +public class Tag { + private String id; + private String name; + + public Tag(String name) { + this.id = UUID.randomUUID().toString(); + this.name = name; + } +} diff --git a/src/main/java/io/spring/core/user/User.java b/src/main/java/io/spring/core/user/User.java index 3a909fb..3060068 100644 --- a/src/main/java/io/spring/core/user/User.java +++ b/src/main/java/io/spring/core/user/User.java @@ -9,7 +9,7 @@ import java.util.UUID; @Getter @NoArgsConstructor -@EqualsAndHashCode(of = {"username"}) +@EqualsAndHashCode(of = {"id"}) public class User { private String id; private String email; diff --git a/src/main/java/io/spring/infrastructure/article/ArticleMapper.java b/src/main/java/io/spring/infrastructure/article/ArticleMapper.java new file mode 100644 index 0000000..37e6008 --- /dev/null +++ b/src/main/java/io/spring/infrastructure/article/ArticleMapper.java @@ -0,0 +1,21 @@ +package io.spring.infrastructure.article; + +import io.spring.core.article.Article; +import io.spring.core.article.Tag; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface ArticleMapper { + void insert(@Param("article") Article article); + + Article findById(@Param("id") String id); + + boolean findTag(@Param("tagName") String tagName); + + void insertTag(@Param("tag") Tag tag); + + void insertArticleTagRelation(@Param("articleId") String articleId, @Param("tagId") String tagId); +} diff --git a/src/main/java/io/spring/infrastructure/article/MyBatisArticleRepository.java b/src/main/java/io/spring/infrastructure/article/MyBatisArticleRepository.java new file mode 100644 index 0000000..ae06730 --- /dev/null +++ b/src/main/java/io/spring/infrastructure/article/MyBatisArticleRepository.java @@ -0,0 +1,38 @@ +package io.spring.infrastructure.article; + +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.article.Tag; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public class MyBatisArticleRepository implements ArticleRepository { + private ArticleMapper articleMapper; + + public MyBatisArticleRepository(ArticleMapper articleMapper) { + this.articleMapper = articleMapper; + } + + @Override + public String toSlug(String title) { + return title.toLowerCase().replace(' ', '-'); + } + + @Override + public void save(Article article) { + articleMapper.insert(article); + for (Tag tag : article.getTags()) { + if (!articleMapper.findTag(tag.getName())) { + articleMapper.insertTag(tag); + } + articleMapper.insertArticleTagRelation(article.getId(), tag.getId()); + } + } + + @Override + public Optional
findById(String id) { + return Optional.ofNullable(articleMapper.findById(id)); + } +} diff --git a/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java b/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java new file mode 100644 index 0000000..2b419f7 --- /dev/null +++ b/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java @@ -0,0 +1,44 @@ +package io.spring.infrastructure.mybatis; + +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; +import org.joda.time.DateTime; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.TimeZone; + +@MappedTypes(DateTime.class) +public class DateTimeHandler implements TypeHandler { + + private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + @Override + public void setParameter(PreparedStatement ps, int i, DateTime parameter, JdbcType jdbcType) throws SQLException { + ps.setTimestamp(i, parameter != null ? new Timestamp(parameter.getMillis()) : null, UTC_CALENDAR); + } + + @Override + public DateTime getResult(ResultSet rs, String columnName) throws SQLException { + Timestamp timestamp = rs.getTimestamp(columnName, UTC_CALENDAR); + return timestamp != null ? new DateTime(timestamp.getTime()) : null; + } + + @Override + public DateTime getResult(ResultSet rs, int columnIndex) throws SQLException { + Timestamp timestamp = rs.getTimestamp(columnIndex, UTC_CALENDAR); + return timestamp != null ? new DateTime(timestamp.getTime()) : null; + } + + @Override + public DateTime getResult(CallableStatement cs, int columnIndex) throws SQLException { + Timestamp ts = cs.getTimestamp(columnIndex, UTC_CALENDAR); + return ts != null ? new DateTime(ts.getTime()) : null; + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d6c5a86..56c3b6d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,4 +4,4 @@ image.default=https://static.productionready.io/images/smiley-cyrus.jpg jwt.secret=nRvyYC4soFxBdZ-F-5Nnzz5USXstR1YylsTd-mA0aKtI9HUlriGrtkf-TiuDapkLiUCogO3JOK7kwZisrHp6wA jwt.sessionTime=86400 mybatis.config-location=classpath:mybatis-config.xml -mybatis.mapper-locations=mapper/*Mapper.xml \ No newline at end of file +mybatis.mapper-locations=mapper/*.xml \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__create_tables.sql b/src/main/resources/db/migration/V1__create_tables.sql index 02ab196..f84aa9a 100644 --- a/src/main/resources/db/migration/V1__create_tables.sql +++ b/src/main/resources/db/migration/V1__create_tables.sql @@ -6,3 +6,34 @@ create table users ( bio text, image varchar(511) ); + +create table articles ( + id varchar(255) primary key, + user_id varchar(255), + slug varchar(255) UNIQUE, + title varchar(255), + description text, + body text, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create table article_favorites ( + article_id varchar(255) not null, + user_id varchar(255) not null +); + +create table follows ( + user_id varchar(255) not null, + follow_id varchar(255) not null +); + +create table tags ( + id varchar(255) primary key, + name varchar(255) +); + +create table article_tags ( + article_id varchar(255) not null, + tag_id varchar(255) not null +); diff --git a/src/main/resources/mapper/ArticleFavoritesQueryService.xml b/src/main/resources/mapper/ArticleFavoritesQueryService.xml new file mode 100644 index 0000000..e1e3d04 --- /dev/null +++ b/src/main/resources/mapper/ArticleFavoritesQueryService.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/ArticleMapper.xml b/src/main/resources/mapper/ArticleMapper.xml new file mode 100644 index 0000000..8cd04d4 --- /dev/null +++ b/src/main/resources/mapper/ArticleMapper.xml @@ -0,0 +1,60 @@ + + + + + insert into articles(id, slug, title, description, body, user_id, created_at, updated_at) + values( + #{article.id}, + #{article.slug}, + #{article.title}, + #{article.description}, + #{article.body}, + #{article.userId}, + #{article.createdAt}, + #{article.updatedAt}) + + + insert into tags (id, name) values (#{tag.id}, #{tag.name}) + + + insert into article_tags (article_id, tag_id) values(#{articleId}, #{tagId}) + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/ArticleReadService.xml b/src/main/resources/mapper/ArticleReadService.xml new file mode 100644 index 0000000..e226a69 --- /dev/null +++ b/src/main/resources/mapper/ArticleReadService.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserReadService.xml b/src/main/resources/mapper/UserReadService.xml new file mode 100644 index 0000000..511066c --- /dev/null +++ b/src/main/resources/mapper/UserReadService.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserRelationshipQueryService.xml b/src/main/resources/mapper/UserRelationshipQueryService.xml new file mode 100644 index 0000000..b281ad7 --- /dev/null +++ b/src/main/resources/mapper/UserRelationshipQueryService.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mybatis-config.xml b/src/main/resources/mybatis-config.xml index 946065f..ac8a12d 100644 --- a/src/main/resources/mybatis-config.xml +++ b/src/main/resources/mybatis-config.xml @@ -17,6 +17,11 @@ + + + + diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java new file mode 100644 index 0000000..0e91f88 --- /dev/null +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -0,0 +1,134 @@ +package io.spring.api; + +import io.restassured.RestAssured; +import io.spring.application.article.ArticleData; +import io.spring.application.article.ArticleQueryService; +import io.spring.application.profile.ProfileData; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +public class ArticlesApiTest extends TestWithCurrentUser { + @LocalServerPort + private int port; + + @MockBean + private ArticleRepository articleRepository; + + @MockBean + private ArticleQueryService articleQueryService; + + 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(email, username, defaultAvatar); + } + + @Test + public void should_create_article_success() throws Exception { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String body = "You have to believe"; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, body, tagList); + + when(articleRepository.toSlug(eq(title))).thenReturn(slug); + + ArticleData articleData = new ArticleData( + "123", + slug, + title, + description, + body, + false, + 0, + new DateTime(), + new DateTime(), + Arrays.asList(tagList), + new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); + + when(articleQueryService.findById(any(), any())).thenReturn(Optional.of(articleData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles") + .then() + .statusCode(200) + .body("article.title", equalTo(title)) + .body("article.favorited", equalTo(false)) + .body("article.body", equalTo(body)) + .body("article.favoritesCount", equalTo(0)) + .body("article.author.username", equalTo(user.getUsername())) + .body("article.author.id", equalTo(null)); + + verify(articleRepository).save(any()); + } + + @Test + public void should_get_error_message_with_wrong_parameter() throws Exception { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String body = ""; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, body, tagList); + + when(articleRepository.toSlug(eq(title))).thenReturn(slug); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles") + .then() + .statusCode(422) + .body("errors.body[0]", equalTo("can't be empty")); + + } + + private HashMap prepareParam(final String title, final String description, final String body, final String[] tagList) { + return new HashMap() {{ + put("article", new HashMap() {{ + put("title", title); + put("description", description); + put("body", body); + put("tagList", tagList); + }}); + }}; + } +} \ No newline at end of file diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java index de81d01..66f1d2c 100644 --- a/src/test/java/io/spring/api/CurrentUserApiTest.java +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -12,56 +12,35 @@ 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.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) @RunWith(SpringRunner.class) -public class CurrentUserApiTest { - @MockBean - private UserRepository userRepository; - - @MockBean - private UserReadService userReadService; +public class CurrentUserApiTest extends TestWithCurrentUser { @LocalServerPort private int port; - @Autowired - private JwtService jwtService; - private User user; - private UserData userData; - private String token; - private String email; - private String username; - private String defaultAvatar; + 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"; - user = new User(email, username, "123", "", defaultAvatar); - when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(user)); - - userData = new UserData(user.getId(), email, username, "", defaultAvatar); - when(userReadService.findOne(eq(username))).thenReturn(userData); - - token = jwtService.toToken(userData); + userFixture(email, username, defaultAvatar); } @Test diff --git a/src/test/java/io/spring/api/TestWithCurrentUser.java b/src/test/java/io/spring/api/TestWithCurrentUser.java new file mode 100644 index 0000000..a3297e6 --- /dev/null +++ b/src/test/java/io/spring/api/TestWithCurrentUser.java @@ -0,0 +1,39 @@ +package io.spring.api; + +import io.spring.application.JwtService; +import io.spring.application.user.UserData; +import io.spring.application.user.UserReadService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Optional; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; + +class TestWithCurrentUser { + @MockBean + protected UserRepository userRepository; + + @MockBean + protected UserReadService userReadService; + + protected User user; + protected UserData userData; + protected String token; + + @Autowired + private JwtService jwtService; + + protected void userFixture(String email, String username, String defaultAvatar) { + user = new User(email, username, "123", "", defaultAvatar); + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(user)); + + userData = new UserData(user.getId(), email, username, "", defaultAvatar); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); + + token = jwtService.toToken(userData); + } +} diff --git a/src/test/java/io/spring/api/UsersApiTest.java b/src/test/java/io/spring/api/UsersApiTest.java index 039f81b..f7c5a0b 100644 --- a/src/test/java/io/spring/api/UsersApiTest.java +++ b/src/test/java/io/spring/api/UsersApiTest.java @@ -55,7 +55,7 @@ public class UsersApiTest { when(jwtService.toToken(any())).thenReturn("123"); UserData userData = new UserData("123", email, username, "", defaultAvatar); - when(userReadService.findOne(eq(username))).thenReturn(userData); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty()); @@ -178,7 +178,7 @@ public class UsersApiTest { UserData userData = new UserData("123", email, username, "", defaultAvatar); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); - when(userReadService.findOne(eq(username))).thenReturn(userData); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); when(jwtService.toToken(any())).thenReturn("123"); Map param = new HashMap() {{ @@ -212,7 +212,7 @@ public class UsersApiTest { UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); - when(userReadService.findOne(eq(username))).thenReturn(userData); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); Map param = new HashMap() {{ put("user", new HashMap() {{ diff --git a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java new file mode 100644 index 0000000..5181299 --- /dev/null +++ b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java @@ -0,0 +1,52 @@ +package io.spring.application.article; + +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.article.MyBatisArticleRepository; +import io.spring.infrastructure.user.MyBatisUserRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@MybatisTest +@Import({ArticleQueryService.class, MyBatisUserRepository.class, MyBatisArticleRepository.class}) +public class ArticleQueryServiceTest { + @Autowired + private ArticleQueryService queryService; + + @Autowired + private ArticleRepository articleRepository; + + @Autowired + private UserRepository userRepository; + + @Test + public void should_fetch_article_success() throws Exception { + User user = new User("aisensiy@gmail.com", "aisensiy", "123", "", ""); + userRepository.save(user); + + Article article = new Article("test", "test", "desc", "body", new String[]{"java", "spring"}, user.getId()); + articleRepository.save(article); + + Optional optional = queryService.findById(article.getId(), user); + assertThat(optional.isPresent(), is(true)); + ArticleData fetched = optional.get(); + assertThat(fetched.getFavoritesCount(), is(0)); + assertThat(fetched.isFavorited(), is(false)); + assertThat(fetched.getCreatedAt(), notNullValue()); + assertThat(fetched.getUpdatedAt(), notNullValue()); + } +} \ No newline at end of file diff --git a/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java b/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java new file mode 100644 index 0000000..0b15b4c --- /dev/null +++ b/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java @@ -0,0 +1,50 @@ +package io.spring.infrastructure.article; + +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.article.Tag; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.user.MyBatisUserRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@MybatisTest +@RunWith(SpringRunner.class) +@Import({MyBatisArticleRepository.class, MyBatisUserRepository.class}) +public class MyBatisArticleRepositoryTest { + @Autowired + private ArticleRepository articleRepository; + + @Autowired + private UserRepository userRepository; + private User user; + + + @Before + public void setUp() throws Exception { + user = new User("aisensiy@gmail.com", "aisensiy", "123", "bio", "default"); + userRepository.save(user); + } + + @Test + public void should_create_and_fetch_article_success() throws Exception { + Article article = new Article("test", "test", "desc", "body", new String[]{"java", "spring"}, user.getId()); + articleRepository.save(article); + Optional
optional = articleRepository.findById(article.getId()); + assertThat(optional.isPresent(), is(true)); + assertThat(optional.get(), is(article)); + assertThat(optional.get().getTags().contains(new Tag("java")), is(true)); + assertThat(optional.get().getTags().contains(new Tag("spring")), is(true)); + } +} \ No newline at end of file