Skip to content

Commit 2977cd6

Browse files
committed
[#21] PUT /articles/:slug
- Implements ArticleUpdateRequest
1 parent 803c549 commit 2977cd6

File tree

10 files changed

+235
-0
lines changed

10 files changed

+235
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.github.raeperd.realworld.application.article;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
import com.fasterxml.jackson.annotation.JsonTypeName;
5+
import io.github.raeperd.realworld.domain.article.ArticleTitle;
6+
import io.github.raeperd.realworld.domain.article.ArticleUpdateRequest;
7+
import lombok.Value;
8+
9+
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT;
10+
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME;
11+
import static io.github.raeperd.realworld.domain.article.ArticleUpdateRequest.builder;
12+
import static java.util.Optional.ofNullable;
13+
14+
@JsonTypeName("article")
15+
@JsonTypeInfo(include = WRAPPER_OBJECT, use = NAME)
16+
@Value
17+
class ArticlePutRequestDTO {
18+
19+
String title;
20+
String description;
21+
String body;
22+
23+
ArticleUpdateRequest toUpdateRequest() {
24+
return builder().titleToUpdate(ofNullable(title).map(ArticleTitle::of).orElse(null))
25+
.descriptionToUpdate(description)
26+
.bodyToUpdate(body)
27+
.build();
28+
}
29+
}

src/main/java/io/github/raeperd/realworld/application/article/ArticleRestController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public ResponseEntity<ArticleModel> getArticleBySlug(@PathVariable String slug)
6565
.map(ArticleModel::fromArticle));
6666
}
6767

68+
@PutMapping("/articles/{slug}")
69+
public ArticleModel putArticleBySlug(@AuthenticationPrincipal UserJWTPayload jwtPayload,
70+
@PathVariable String slug,
71+
@RequestBody ArticlePutRequestDTO dto) {
72+
final var articleUpdated = articleService.updateArticle(jwtPayload.getUserId(), slug, dto.toUpdateRequest());
73+
return ArticleModel.fromArticle(articleUpdated);
74+
}
75+
6876
@PostMapping("/articles/{slug}/favorite")
6977
public ArticleModel favoriteArticleBySlug(@AuthenticationPrincipal UserJWTPayload jwtPayload,
7078
@PathVariable String slug) {

src/main/java/io/github/raeperd/realworld/domain/article/Article.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public Article afterUserUnFavoritesArticle(User user) {
6666
return updateFavoriteByUser(user);
6767
}
6868

69+
public void updateArticle(ArticleUpdateRequest updateRequest) {
70+
contents.updateArticleContentsIfPresent(updateRequest);
71+
}
72+
6973
public User getAuthor() {
7074
return author;
7175
}

src/main/java/io/github/raeperd/realworld/domain/article/ArticleContents.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public Set<Tag> getTags() {
5353
return tags;
5454
}
5555

56+
void updateArticleContentsIfPresent(ArticleUpdateRequest updateRequest) {
57+
updateRequest.getTitleToUpdate().ifPresent(titleToUpdate -> title = titleToUpdate);
58+
updateRequest.getDescriptionToUpdate().ifPresent(descriptionToUpdate -> description = descriptionToUpdate);
59+
updateRequest.getBodyToUpdate().ifPresent(bodyToUpdate -> body = bodyToUpdate);
60+
}
61+
5662
public void setTags(Set<Tag> tags) {
5763
this.tags = tags;
5864
}

src/main/java/io/github/raeperd/realworld/domain/article/ArticleService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ public Optional<Article> getArticleBySlug(String slug) {
7575
return articleRepository.findFirstByContentsTitleSlug(slug);
7676
}
7777

78+
@Transactional
79+
public Article updateArticle(long userId, String slug, ArticleUpdateRequest request) {
80+
return mapIfAllPresent(userFindService.findById(userId), getArticleBySlug(slug),
81+
(user, article) -> user.updateArticle(article, request))
82+
.orElseThrow(NoSuchElementException::new);
83+
}
84+
7885
@Transactional
7986
public Article favoriteArticle(long userId, String articleSlugToFavorite) {
8087
return mapIfAllPresent(
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.github.raeperd.realworld.domain.article;
2+
3+
import java.util.Optional;
4+
5+
import static java.util.Optional.ofNullable;
6+
7+
public class ArticleUpdateRequest {
8+
9+
private final ArticleTitle titleToUpdate;
10+
private final String descriptionToUpdate;
11+
private final String bodyToUpdate;
12+
13+
public static ArticleUpdateRequestBuilder builder() {
14+
return new ArticleUpdateRequestBuilder();
15+
}
16+
17+
Optional<ArticleTitle> getTitleToUpdate() {
18+
return ofNullable(titleToUpdate);
19+
}
20+
21+
Optional<String> getDescriptionToUpdate() {
22+
return ofNullable(descriptionToUpdate);
23+
}
24+
25+
Optional<String> getBodyToUpdate() {
26+
return ofNullable(bodyToUpdate);
27+
}
28+
29+
private ArticleUpdateRequest(ArticleUpdateRequestBuilder builder) {
30+
this(builder.titleToUpdate, builder.descriptionToUpdate, builder.bodyToUpdate);
31+
}
32+
33+
private ArticleUpdateRequest(ArticleTitle titleToUpdate, String descriptionToUpdate, String bodyToUpdate) {
34+
this.titleToUpdate = titleToUpdate;
35+
this.descriptionToUpdate = descriptionToUpdate;
36+
this.bodyToUpdate = bodyToUpdate;
37+
}
38+
39+
public static class ArticleUpdateRequestBuilder {
40+
private ArticleTitle titleToUpdate;
41+
private String descriptionToUpdate;
42+
private String bodyToUpdate;
43+
44+
public ArticleUpdateRequestBuilder titleToUpdate(ArticleTitle titleToUpdate) {
45+
this.titleToUpdate = titleToUpdate;
46+
return this;
47+
}
48+
public ArticleUpdateRequestBuilder descriptionToUpdate(String descriptionToUpdate) {
49+
this.descriptionToUpdate = descriptionToUpdate;
50+
return this;
51+
}
52+
public ArticleUpdateRequestBuilder bodyToUpdate(String bodyToUpdate) {
53+
this.bodyToUpdate = bodyToUpdate;
54+
return this;
55+
}
56+
57+
public ArticleUpdateRequest build() {
58+
return new ArticleUpdateRequest(this);
59+
}
60+
}
61+
}

src/main/java/io/github/raeperd/realworld/domain/user/User.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.github.raeperd.realworld.domain.article.Article;
44
import io.github.raeperd.realworld.domain.article.ArticleContents;
5+
import io.github.raeperd.realworld.domain.article.ArticleUpdateRequest;
56
import org.springframework.security.crypto.password.PasswordEncoder;
67

78
import javax.persistence.*;
@@ -55,6 +56,14 @@ public Article writeArticle(ArticleContents contents) {
5556
return new Article(this, contents);
5657
}
5758

59+
public Article updateArticle(Article article, ArticleUpdateRequest request) {
60+
if (article.getAuthor() != this) {
61+
throw new IllegalAccessError("Not authorized to update this article");
62+
}
63+
article.updateArticle(request);
64+
return article;
65+
}
66+
5867
public Article favoriteArticle(Article articleToFavorite) {
5968
articleFavorited.add(articleToFavorite);
6069
return articleToFavorite.afterUserFavoritesArticle(this);

src/test/java/io/github/raeperd/realworld/IntegrationTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ void get_single_article_by_slug() throws Exception {
198198
.andExpect(validSingleArticleModel());
199199
}
200200

201+
@Order(11)
202+
@Test
203+
void put_article() throws Exception {
204+
mockMvc.perform(put("/articles/{slug}", "how-to-train-your-dragon")
205+
.header(AUTHORIZATION, "Token " + token)
206+
.contentType(APPLICATION_JSON)
207+
.content("{\"article\":{\"body\":\"With two hands\"}}"))
208+
.andExpect(status().isOk())
209+
.andExpect(validSingleArticleModel())
210+
.andExpect(jsonPath("article.body", is("With two hands")));
211+
}
212+
201213
@Order(12)
202214
@Test
203215
void post_favorite_article() throws Exception {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.github.raeperd.realworld.domain.article;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.ExtendWith;
5+
import org.mockito.Mock;
6+
import org.mockito.junit.jupiter.MockitoExtension;
7+
8+
import static io.github.raeperd.realworld.domain.article.ArticleUpdateRequest.builder;
9+
import static java.util.Collections.emptySet;
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
@ExtendWith(MockitoExtension.class)
13+
class ArticleContentsTest {
14+
15+
16+
@Test
17+
void when_updateArticle_with_no_update_field_request_expect_not_changed() {
18+
final var articleContents = sampleArticleContents();
19+
final var emptyUpdateRequest = builder().build();
20+
21+
articleContents.updateArticleContentsIfPresent(emptyUpdateRequest);
22+
23+
assertThatEqualArticleContents(articleContents, sampleArticleContents());
24+
}
25+
26+
@Test
27+
void when_updateArticle_with_all_field_expect_changed(@Mock ArticleTitle titleToUpdate) {
28+
final var articleContents = sampleArticleContents();
29+
final var fullUpdateRequest = builder().titleToUpdate(titleToUpdate)
30+
.descriptionToUpdate("descriptionToUpdate")
31+
.bodyToUpdate("bodyToUpdate")
32+
.build();
33+
34+
articleContents.updateArticleContentsIfPresent(fullUpdateRequest);
35+
36+
assertThat(articleContents.getTitle()).isEqualTo(titleToUpdate);
37+
assertThat(articleContents.getDescription()).isEqualTo("descriptionToUpdate");
38+
assertThat(articleContents.getBody()).isEqualTo("bodyToUpdate");
39+
}
40+
41+
private ArticleContents sampleArticleContents() {
42+
return new ArticleContents("description", ArticleTitle.of("title"), "body", emptySet());
43+
}
44+
45+
private void assertThatEqualArticleContents(ArticleContents left, ArticleContents right) {
46+
assertThat(equalsArticleContents(left, right)).isTrue();
47+
}
48+
49+
private boolean equalsArticleContents(ArticleContents left, ArticleContents right) {
50+
if (!left.getTitle().equals(right.getTitle())) {
51+
return false;
52+
}
53+
if (!left.getDescription().equals(right.getDescription())) {
54+
return false;
55+
}
56+
if (!left.getBody().equals(right.getBody())) {
57+
return false;
58+
}
59+
return left.getTags().equals(right.getTags());
60+
}
61+
62+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.github.raeperd.realworld.domain.article;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.ExtendWith;
5+
import org.mockito.Mock;
6+
import org.mockito.junit.jupiter.MockitoExtension;
7+
8+
import static io.github.raeperd.realworld.domain.article.ArticleUpdateRequest.builder;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
@ExtendWith(MockitoExtension.class)
12+
class ArticleUpdateRequestTest {
13+
14+
@Test
15+
void when_articleUpdateRequest_created_without_field_expect_get_return_empty() {
16+
final var requestWithoutFields = builder().build();
17+
18+
assertThat(requestWithoutFields.getTitleToUpdate()).isEmpty();
19+
assertThat(requestWithoutFields.getDescriptionToUpdate()).isEmpty();
20+
assertThat(requestWithoutFields.getBodyToUpdate()).isEmpty();
21+
}
22+
23+
@Test
24+
void when_articleUpdateRequest_created_with_all_fields_expect_all_fields(@Mock ArticleTitle title) {
25+
final var requestWithAllFields = builder()
26+
.titleToUpdate(title)
27+
.descriptionToUpdate("descriptionToUpdate")
28+
.bodyToUpdate("bodyToUpdate")
29+
.build();
30+
31+
assertThat(requestWithAllFields).hasNoNullFieldsOrProperties();
32+
assertThat(requestWithAllFields.getTitleToUpdate()).contains(title);
33+
assertThat(requestWithAllFields.getDescriptionToUpdate()).contains("descriptionToUpdate");
34+
assertThat(requestWithAllFields.getBodyToUpdate()).contains("bodyToUpdate");
35+
}
36+
37+
}

0 commit comments

Comments
 (0)